From e423a362dd873e7b8780efe2700a4673840fa2ba Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 3 Sep 2024 14:55:53 +0200 Subject: [PATCH 01/20] Change static vars to member vars --- .../native_app_start_event_processor.dart | 18 +++++--- .../native_app_start_integration.dart | 31 ++++---------- .../navigation/sentry_navigator_observer.dart | 15 +++---- flutter/lib/src/sentry_flutter.dart | 6 ++- .../native_app_start_integration_test.dart | 41 ++++++++----------- 5 files changed, 52 insertions(+), 59 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 a0da42359e..0472fc6ea5 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 @@ -18,7 +18,15 @@ class NativeAppStartEventProcessor implements EventProcessor { @override Future apply(SentryEvent event, Hint hint) async { final options = _hub.options; - if (NativeAppStartIntegration.didAddAppStartMeasurement || + + final integrations = + options.integrations.whereType(); + if (integrations.isEmpty) { + return event; + } + final nativeAppStartIntegration = integrations.first; + + if (nativeAppStartIntegration.didAddAppStartMeasurement || event is! SentryTransaction || options is! SentryFlutterOptions) { return event; @@ -26,22 +34,22 @@ class NativeAppStartEventProcessor implements EventProcessor { AppStartInfo? appStartInfo; if (!options.autoAppStart) { - final appStartEnd = NativeAppStartIntegration.appStartEnd; + final appStartEnd = nativeAppStartIntegration.appStartEnd; if (appStartEnd != null) { - appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); + appStartInfo = await nativeAppStartIntegration.getAppStartInfo(); appStartInfo?.end = appStartEnd; } else { // If autoAppStart is disabled and appStartEnd is not set, we can't add app starts return event; } } else { - appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); + appStartInfo = await nativeAppStartIntegration.getAppStartInfo(); } final measurement = appStartInfo?.toMeasurement(); if (measurement != null) { event.measurements[measurement.name] = measurement; - NativeAppStartIntegration.didAddAppStartMeasurement = true; + nativeAppStartIntegration.didAddAppStartMeasurement = true; } if (appStartInfo != null) { diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index af747dbf36..25551bbde9 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -24,14 +24,14 @@ class NativeAppStartIntegration extends Integration { /// [SentryFlutterOptions.autoAppStart] is true, or by calling /// [SentryFlutter.setAppStartEnd] @internal - static DateTime? appStartEnd; + DateTime? appStartEnd; /// Flag indicating if app start was already fetched. - static bool _didFetchAppStart = false; + bool _didFetchAppStart = false; /// Flag indicating if app start measurement was added to the first transaction. @internal - static bool didAddAppStartMeasurement = false; + bool didAddAppStartMeasurement = false; /// Timeout duration to wait for the app start info to be fetched. static const _timeoutDuration = Duration(seconds: 10); @@ -42,15 +42,14 @@ class NativeAppStartIntegration extends Integration { /// We filter out App starts more than 60s static const _maxAppStartMillis = 60000; - static Completer _appStartCompleter = - Completer(); - static AppStartInfo? _appStartInfo; + Completer _appStartCompleter = Completer(); + AppStartInfo? _appStartInfo; @internal - static bool isIntegrationTest = false; + bool isIntegrationTest = false; @internal - static void setAppStartInfo(AppStartInfo? appStartInfo) { + void setAppStartInfo(AppStartInfo? appStartInfo) { _appStartInfo = appStartInfo; if (_appStartCompleter.isCompleted) { _appStartCompleter = Completer(); @@ -59,7 +58,7 @@ class NativeAppStartIntegration extends Integration { } @internal - static Future getAppStartInfo() { + Future getAppStartInfo() { if (_appStartInfo != null) { return Future.value(_appStartInfo); } @@ -67,20 +66,6 @@ class NativeAppStartIntegration extends Integration { .timeout(_timeoutDuration, onTimeout: () => null); } - @visibleForTesting - static void clearAppStartInfo() { - _appStartInfo = null; - _appStartCompleter = Completer(); - didAddAppStartMeasurement = false; - } - - /// Reset state - @visibleForTesting - static void reset() { - appStartEnd = null; - _didFetchAppStart = false; - } - @override void call(Hub hub, SentryFlutterOptions options) { if (isIntegrationTest) { diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index db91098407..a2d6adb413 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -350,13 +350,14 @@ class SentryNavigatorObserver extends RouteObserver> { DateTime startTimestamp = _hub.options.clock(); DateTime? endTimestamp; - if (isAppStart) { - final appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); - if (appStartInfo == null) return; - - startTimestamp = appStartInfo.start; - endTimestamp = appStartInfo.end; - } + // TODO: Handle in app_start_event_processor + // if (isAppStart) { + // final appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); + // if (appStartInfo == null) return; + // + // startTimestamp = appStartInfo.start; + // endTimestamp = appStartInfo.end; + // } await _startTransaction(route, startTimestamp); diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 29f533d082..d43027c500 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -226,7 +226,11 @@ mixin SentryFlutter { /// Manually set when your app finished startup. Make sure to set /// [SentryFlutterOptions.autoAppStart] to false on init. static void setAppStartEnd(DateTime appStartEnd) { - NativeAppStartIntegration.appStartEnd = appStartEnd; + // ignore: invalid_use_of_internal_member + final integrations = Sentry.currentHub.options.integrations + .whereType(); + integrations + .forEach((integration) => integration.appStartEnd = appStartEnd); } static void _setSdk(SentryFlutterOptions options) { diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index 8fc85e6157..3179c08c6a 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -31,7 +31,7 @@ void main() { // This ensures that setAppStartInfo has been called, which happens asynchronously // in a post-frame callback. Waiting here prevents race conditions in subsequent tests // that might depend on or modify the app start info. - await NativeAppStartIntegration.getAppStartInfo(); + await fixture.sut.getAppStartInfo(); } group('$NativeAppStartIntegration', () { @@ -39,6 +39,8 @@ void main() { setUp(() { fixture = Fixture(); + fixture.options.addIntegration(fixture.sut); + setupMocks(fixture); when(fixture.binding.fetchNativeAppStart()).thenAnswer((_) async => NativeAppStart( @@ -46,12 +48,10 @@ void main() { pluginRegistrationTime: 10, isColdStart: true, nativeSpanTimes: {})); - NativeAppStartIntegration.clearAppStartInfo(); }); test('native app start measurement added to first transaction', () async { - NativeAppStartIntegration.appStartEnd = - DateTime.fromMillisecondsSinceEpoch(10); + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); await registerIntegration(fixture); final tracer = fixture.createTracer(); @@ -68,8 +68,7 @@ void main() { test('native app start measurement not added to following transactions', () async { - NativeAppStartIntegration.appStartEnd = - DateTime.fromMillisecondsSinceEpoch(10); + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); await registerIntegration(fixture); final tracer = fixture.createTracer(); @@ -86,8 +85,7 @@ void main() { }); test('measurements appended', () async { - NativeAppStartIntegration.appStartEnd = - DateTime.fromMillisecondsSinceEpoch(10); + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); final measurement = SentryMeasurement.warmAppStart(Duration(seconds: 1)); await registerIntegration(fixture); @@ -107,8 +105,7 @@ void main() { }); test('native app start measurement not added if more than 60s', () async { - NativeAppStartIntegration.appStartEnd = - DateTime.fromMillisecondsSinceEpoch(60001); + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(60001); await registerIntegration(fixture); final tracer = fixture.createTracer(); @@ -123,11 +120,10 @@ void main() { test('native app start integration is called and sets app start info', () async { - NativeAppStartIntegration.appStartEnd = - DateTime.fromMillisecondsSinceEpoch(10); + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); await registerIntegration(fixture); - final appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); + final appStartInfo = await fixture.sut.getAppStartInfo(); expect(appStartInfo?.start, DateTime.fromMillisecondsSinceEpoch(0)); expect(appStartInfo?.end, DateTime.fromMillisecondsSinceEpoch(10)); }); @@ -157,6 +153,7 @@ void main() { fixture = Fixture( frameCallbackTimeout: NativeAppStartIntegration.timeoutDuration + const Duration(seconds: 5)); + fixture.options.addIntegration(fixture.sut); fixture.options.autoAppStart = false; await registerIntegration(fixture); @@ -179,7 +176,9 @@ void main() { fixture.options.autoAppStart = false; await registerIntegration(fixture); - SentryFlutter.setAppStartEnd(DateTime.fromMillisecondsSinceEpoch(10)); + + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); + // SentryFlutter.setAppStartEnd(DateTime.fromMillisecondsSinceEpoch(10)); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); @@ -192,7 +191,7 @@ void main() { expect(measurement.value, 10); expect(measurement.unit, DurationSentryMeasurementUnit.milliSecond); - final appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); + final appStartInfo = await fixture.sut.getAppStartInfo(); final appStartSpan = enriched.spans.firstWhereOrNull((element) => element.context.description == appStartInfo!.appStartTypeDescription); @@ -260,10 +259,8 @@ void main() { setUp(() async { fixture = Fixture(); - NativeAppStartIntegration.clearAppStartInfo(); - - NativeAppStartIntegration.appStartEnd = - DateTime.fromMillisecondsSinceEpoch(50); + fixture.options.addIntegration(fixture.sut); + fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(50); // dartLoadingEnd needs to be set after engine end (see MockNativeChannel) SentryFlutter.sentrySetupStartTime = @@ -281,7 +278,7 @@ void main() { enriched = await processor.apply(transaction, Hint()) as SentryTransaction; - final appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); + final appStartInfo = await fixture.sut.getAppStartInfo(); coldStartSpan = enriched.spans.firstWhereOrNull((element) => element.context.description == appStartInfo?.appStartTypeDescription); @@ -395,8 +392,7 @@ void main() { final engineReadyEndtime = DateTime.fromMillisecondsSinceEpoch( appStartInfoSrc.pluginRegistrationTime.toInt()) .toUtc(); - expect(coldStartSpan?.endTimestamp, - NativeAppStartIntegration.appStartEnd?.toUtc()); + expect(coldStartSpan?.endTimestamp, fixture.sut.appStartEnd?.toUtc()); expect(pluginRegistrationSpan?.endTimestamp, engineReadyEndtime); expect(sentrySetupSpan?.endTimestamp, SentryFlutter.sentrySetupStartTime?.toUtc()); @@ -415,7 +411,6 @@ class Fixture extends IntegrationTestFixture { FakeFrameCallbackHandler( finishAfterDuration: frameCallbackTimeout ?? const Duration(milliseconds: 50)))) { - NativeAppStartIntegration.reset(); hub = MockHub(); // ignore: invalid_use_of_internal_member when(hub.options).thenReturn(options); From 77e1e11c79917597e87cddec9f9933f8fabf7933 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 3 Sep 2024 15:00:36 +0200 Subject: [PATCH 02/20] leave integration test for now --- flutter/lib/src/integrations/native_app_start_integration.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 25551bbde9..a3c62d36fd 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -46,7 +46,7 @@ class NativeAppStartIntegration extends Integration { AppStartInfo? _appStartInfo; @internal - bool isIntegrationTest = false; + static bool isIntegrationTest = false; @internal void setAppStartInfo(AppStartInfo? appStartInfo) { From 7246d5ef13e71a930a042620586eba3d458f430e Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 3 Sep 2024 17:57:11 +0200 Subject: [PATCH 03/20] remove completer --- flutter/example/ios/Runner/AppDelegate.swift | 2 +- .../native_app_start_event_processor.dart | 175 ++++++++++++++-- .../native_app_start_integration.dart | 191 +++++++----------- flutter/lib/src/sentry_flutter.dart | 12 ++ .../native_app_start_integration_test.dart | 78 ++++--- flutter/test/mock_frame_callback_handler.dart | 25 +++ 6 files changed, 316 insertions(+), 167 deletions(-) create mode 100644 flutter/test/mock_frame_callback_handler.dart diff --git a/flutter/example/ios/Runner/AppDelegate.swift b/flutter/example/ios/Runner/AppDelegate.swift index a231cc9c60..c24cacbbb2 100644 --- a/flutter/example/ios/Runner/AppDelegate.swift +++ b/flutter/example/ios/Runner/AppDelegate.swift @@ -2,7 +2,7 @@ import UIKit import Flutter import Sentry -@UIApplicationMain +@main @objc class AppDelegate: FlutterAppDelegate { private let _channel = "example.flutter.sentry.io" 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 0472fc6ea5..b7c4f7d54f 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 @@ -26,35 +26,31 @@ class NativeAppStartEventProcessor implements EventProcessor { } final nativeAppStartIntegration = integrations.first; - if (nativeAppStartIntegration.didAddAppStartMeasurement || - event is! SentryTransaction || - options is! SentryFlutterOptions) { + if (event is! SentryTransaction || options is! SentryFlutterOptions) { + return event; + } + + AppStartInfo? appStartInfo = nativeAppStartIntegration.appStartInfo; + if (appStartInfo == null) { return event; } - AppStartInfo? appStartInfo; if (!options.autoAppStart) { final appStartEnd = nativeAppStartIntegration.appStartEnd; if (appStartEnd != null) { - appStartInfo = await nativeAppStartIntegration.getAppStartInfo(); - appStartInfo?.end = appStartEnd; + appStartInfo.end = appStartEnd; } else { // If autoAppStart is disabled and appStartEnd is not set, we can't add app starts return event; } - } else { - appStartInfo = await nativeAppStartIntegration.getAppStartInfo(); } - final measurement = appStartInfo?.toMeasurement(); + final measurement = appStartInfo.toMeasurement(); if (measurement != null) { event.measurements[measurement.name] = measurement; - nativeAppStartIntegration.didAddAppStartMeasurement = true; } - if (appStartInfo != null) { - await _attachAppStartSpans(appStartInfo, event.tracer); - } + await _attachAppStartSpans(appStartInfo, event.tracer); return event; } @@ -158,3 +154,156 @@ class NativeAppStartEventProcessor implements EventProcessor { return span; } } + +class NativeAppStartHandler { + final Hub _hub; + + NativeAppStartHandler({Hub? hub}) : _hub = hub ?? HubAdapter(); + + Future call( + AppStartInfo appStartInfo, + SentryFlutterOptions options, + ) async { + const screenName = SentryNavigatorObserver.rootScreenName; + final transaction = _hub.startTransaction( + screenName, + SentrySpanOperations.uiLoad, + startTimestamp: appStartInfo.start, + ); + final ttidSpan = transaction.startChild( + SentrySpanOperations.uiTimeToInitialDisplay, + description: '$screenName initial display', + startTimestamp: appStartInfo.start, + ); + await ttidSpan.finish(endTimestamp: appStartInfo.end); + await transaction.finish(endTimestamp: appStartInfo.end); + + if (!options.autoAppStart) { + final appStartEnd = SentryFlutter.appStatEnd; + if (appStartEnd != null) { + appStartInfo.end = appStartEnd; + } else { + // If autoAppStart is disabled and appStartEnd is not set, we can't add app starts + return; + } + } + + await attachMeasurements(transaction as SentryTransaction, appStartInfo); + await attachSpans(transaction as SentryTransaction, appStartInfo); + } + + Future attachMeasurements( + SentryTransaction transaction, AppStartInfo appStartInfo) async { + final measurement = appStartInfo.toMeasurement(); + if (measurement != null) { + transaction.measurements[measurement.name] = measurement; + } + } + + Future attachSpans( + SentryTransaction transaction, AppStartInfo appStartInfo) async { + SentryTracer tracer = transaction.tracer; + + final transactionTraceId = tracer.context.traceId; + final appStartEnd = appStartInfo.end; + if (appStartEnd == null) { + return; + } + + final appStartSpan = await _createAndFinishSpan( + tracer: tracer, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.appStartTypeDescription, + parentSpanId: tracer.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.start, + endTimestamp: appStartEnd, + ); + + await _attachNativeSpans(appStartInfo, tracer, appStartSpan); + + final pluginRegistrationSpan = await _createAndFinishSpan( + tracer: tracer, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.pluginRegistrationDescription, + parentSpanId: appStartSpan.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.start, + endTimestamp: appStartInfo.pluginRegistration, + ); + + final sentrySetupSpan = await _createAndFinishSpan( + tracer: tracer, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.sentrySetupDescription, + parentSpanId: appStartSpan.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.pluginRegistration, + endTimestamp: appStartInfo.sentrySetupStart, + ); + + final firstFrameRenderSpan = await _createAndFinishSpan( + tracer: tracer, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.firstFrameRenderDescription, + parentSpanId: appStartSpan.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.sentrySetupStart, + endTimestamp: appStartEnd, + ); + + tracer.children.addAll([ + appStartSpan, + pluginRegistrationSpan, + sentrySetupSpan, + firstFrameRenderSpan, + ]); + } + + Future _attachNativeSpans(AppStartInfo appStartInfo, + SentryTracer transaction, SentrySpan parent) async { + await Future.forEach(appStartInfo.nativeSpanTimes, + (timeSpan) async { + try { + final span = await _createAndFinishSpan( + tracer: transaction, + operation: appStartInfo.appStartTypeOperation, + description: timeSpan.description, + parentSpanId: parent.context.spanId, + traceId: transaction.context.traceId, + startTimestamp: timeSpan.start, + endTimestamp: timeSpan.end, + ); + span.data.putIfAbsent('native', () => true); + transaction.children.add(span); + } catch (e) { + _hub.options.logger(SentryLevel.warning, + 'Failed to attach native span to app start transaction: $e'); + } + }); + } + + Future _createAndFinishSpan({ + required SentryTracer tracer, + required String operation, + required String description, + required SpanId parentSpanId, + required SentryId traceId, + required DateTime startTimestamp, + required DateTime endTimestamp, + }) async { + final span = SentrySpan( + tracer, + SentrySpanContext( + operation: operation, + description: description, + parentSpanId: parentSpanId, + traceId: traceId, + ), + _hub, + startTimestamp: startTimestamp, + ); + await span.finish(endTimestamp: endTimestamp); + return span; + } +} diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index a3c62d36fd..1a99eea87b 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -1,11 +1,9 @@ // ignore_for_file: invalid_use_of_internal_member - -import 'dart:async'; - import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; +import '../native/native_app_start.dart'; import '../native/sentry_native_binding.dart'; import '../event_processor/native_app_start_event_processor.dart'; @@ -26,9 +24,6 @@ class NativeAppStartIntegration extends Integration { @internal DateTime? appStartEnd; - /// Flag indicating if app start was already fetched. - bool _didFetchAppStart = false; - /// Flag indicating if app start measurement was added to the first transaction. @internal bool didAddAppStartMeasurement = false; @@ -42,29 +37,13 @@ class NativeAppStartIntegration extends Integration { /// We filter out App starts more than 60s static const _maxAppStartMillis = 60000; - Completer _appStartCompleter = Completer(); AppStartInfo? _appStartInfo; @internal static bool isIntegrationTest = false; @internal - void setAppStartInfo(AppStartInfo? appStartInfo) { - _appStartInfo = appStartInfo; - if (_appStartCompleter.isCompleted) { - _appStartCompleter = Completer(); - } - _appStartCompleter.complete(appStartInfo); - } - - @internal - Future getAppStartInfo() { - if (_appStartInfo != null) { - return Future.value(_appStartInfo); - } - return _appStartCompleter.future - .timeout(_timeoutDuration, onTimeout: () => null); - } + AppStartInfo? get appStartInfo => _appStartInfo; @override void call(Hub hub, SentryFlutterOptions options) { @@ -78,113 +57,99 @@ class NativeAppStartIntegration extends Integration { sentrySetupStart: DateTime.now().add(const Duration(milliseconds: 60)), nativeSpanTimes: [], ); - setAppStartInfo(appStartInfo); - return; - } - - if (_didFetchAppStart) { + _appStartInfo = appStartInfo; return; } _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - _didFetchAppStart = true; final nativeAppStart = await _native.fetchNativeAppStart(); if (nativeAppStart == null) { - setAppStartInfo(null); return; } - - final sentrySetupStartDateTime = SentryFlutter.sentrySetupStartTime; - if (sentrySetupStartDateTime == null) { - setAppStartInfo(null); + final appStartInfo = _infoNativeAppStart(nativeAppStart, options); + if (appStartInfo == null) { return; } + _appStartInfo = appStartInfo; + + const screenName = SentryNavigatorObserver.rootScreenName; + final transaction = hub.startTransaction( + screenName, SentrySpanOperations.uiLoad, + startTimestamp: appStartInfo.start); + final ttidSpan = transaction.startChild( + SentrySpanOperations.uiTimeToInitialDisplay, + description: '$screenName initial display', + startTimestamp: appStartInfo.start); + await ttidSpan.finish(endTimestamp: appStartInfo.end); + await transaction.finish(endTimestamp: appStartInfo.end); + }); - final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.appStartTime.toInt()); - final pluginRegistrationDateTime = DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.pluginRegistrationTime); - - if (options.autoAppStart) { - // We only assign the current time if it's not already set - this is useful in tests - appStartEnd ??= options.clock(); - - final duration = appStartEnd?.difference(appStartDateTime); - - // 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 (duration != null && duration.inMilliseconds > _maxAppStartMillis) { - setAppStartInfo(null); - return; - } - } + options.addEventProcessor(NativeAppStartEventProcessor(hub: hub)); - List nativeSpanTimes = []; - for (final entry in nativeAppStart.nativeSpanTimes.entries) { - try { - final startTimestampMs = - entry.value['startTimestampMsSinceEpoch'] as int; - final endTimestampMs = - entry.value['stopTimestampMsSinceEpoch'] as int; - nativeSpanTimes.add(TimeSpan( - start: DateTime.fromMillisecondsSinceEpoch(startTimestampMs), - end: DateTime.fromMillisecondsSinceEpoch(endTimestampMs), - description: entry.key as String, - )); - } catch (e) { - _hub.options.logger( - SentryLevel.warning, 'Failed to parse native span times: $e'); - continue; - } - } + options.sdk.addIntegration('nativeAppStartIntegration'); + } - // We want to sort because the native spans are not guaranteed to be in order. - // Performance wise this won't affect us since the native span amount is very low. - nativeSpanTimes.sort((a, b) => a.start.compareTo(b.start)); + AppStartInfo? _infoNativeAppStart( + NativeAppStart nativeAppStart, SentryFlutterOptions options) { + final sentrySetupStartDateTime = SentryFlutter.sentrySetupStartTime; + if (sentrySetupStartDateTime == null) { + return null; + } - final appStartInfo = AppStartInfo( - nativeAppStart.isColdStart ? AppStartType.cold : AppStartType.warm, - start: appStartDateTime, - end: appStartEnd, - pluginRegistration: pluginRegistrationDateTime, - sentrySetupStart: sentrySetupStartDateTime, - nativeSpanTimes: nativeSpanTimes); - - setAppStartInfo(appStartInfo); - - // When we don't have a SentryNavigatorObserver, a TTID transaction - // is not created therefore we need to create a transaction ourselves. - // We detect this by checking if the currentRouteName is null. - // This is a workaround since there is no api that tells us if - // the navigator observer exists and has been attached. - // The navigator observer also triggers much earlier so if it was attached - // it would have already set the routeName and the isCreated flag. - // The currentRouteName is always set during a didPush triggered - // by the navigator observer. - if (!SentryNavigatorObserver.isCreated && - SentryNavigatorObserver.currentRouteName == null) { - const screenName = SentryNavigatorObserver.rootScreenName; - final transaction = hub.startTransaction( - screenName, SentrySpanOperations.uiLoad, - startTimestamp: appStartInfo.start); - final ttidSpan = transaction.startChild( - SentrySpanOperations.uiTimeToInitialDisplay, - description: '$screenName initial display', - startTimestamp: appStartInfo.start); - await ttidSpan.finish(endTimestamp: appStartInfo.end); - await transaction.finish(endTimestamp: appStartInfo.end); + final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.appStartTime.toInt()); + final pluginRegistrationDateTime = DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.pluginRegistrationTime); + + if (options.autoAppStart) { + // We only assign the current time if it's not already set - this is useful in tests + appStartEnd ??= options.clock(); + + final duration = appStartEnd?.difference(appStartDateTime); + + // 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 (duration != null && duration.inMilliseconds > _maxAppStartMillis) { + return null; } - }); + } - options.addEventProcessor(NativeAppStartEventProcessor(hub: hub)); + List nativeSpanTimes = []; + for (final entry in nativeAppStart.nativeSpanTimes.entries) { + try { + final startTimestampMs = + entry.value['startTimestampMsSinceEpoch'] as int; + final endTimestampMs = entry.value['stopTimestampMsSinceEpoch'] as int; + nativeSpanTimes.add(TimeSpan( + start: DateTime.fromMillisecondsSinceEpoch(startTimestampMs), + end: DateTime.fromMillisecondsSinceEpoch(endTimestampMs), + description: entry.key as String, + )); + } catch (e) { + _hub.options.logger( + SentryLevel.warning, 'Failed to parse native span times: $e'); + continue; + } + } - options.sdk.addIntegration('nativeAppStartIntegration'); + // We want to sort because the native spans are not guaranteed to be in order. + // Performance wise this won't affect us since the native span amount is very low. + nativeSpanTimes.sort((a, b) => a.start.compareTo(b.start)); + + return AppStartInfo( + nativeAppStart.isColdStart ? AppStartType.cold : AppStartType.warm, + start: appStartDateTime, + end: appStartEnd, + pluginRegistration: pluginRegistrationDateTime, + sentrySetupStart: sentrySetupStartDateTime, + nativeSpanTimes: nativeSpanTimes, + ); } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index d43027c500..40dbff1663 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -233,6 +233,18 @@ mixin SentryFlutter { .forEach((integration) => integration.appStartEnd = appStartEnd); } + static DateTime? get appStatEnd { + // ignore: invalid_use_of_internal_member + final integrations = Sentry.currentHub.options.integrations + .whereType(); + + if (integrations.isNotEmpty) { + return integrations.first.appStartEnd; + } else { + return null; + } + } + static void _setSdk(SentryFlutterOptions options) { // overwrite sdk info with current flutter sdk final sdk = SdkVersion( diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index 3179c08c6a..5c18295aea 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -10,9 +10,9 @@ import 'package:sentry_flutter/src/integrations/native_app_start_integration.dar import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry_flutter/src/native/native_app_start.dart'; -import '../fake_frame_callback_handler.dart'; +import '../mock_frame_callback_handler.dart'; +import '../mocks.dart'; import '../mocks.mocks.dart'; -import 'fixture.dart'; void main() { void setupMocks(Fixture fixture) { @@ -25,15 +25,6 @@ void main() { .thenAnswer((_) async => SentryId.empty()); } - Future registerIntegration(Fixture fixture) async { - await fixture.registerIntegration(); - // Wait for the app start info to be fetched - // This ensures that setAppStartInfo has been called, which happens asynchronously - // in a post-frame callback. Waiting here prevents race conditions in subsequent tests - // that might depend on or modify the app start info. - await fixture.sut.getAppStartInfo(); - } - group('$NativeAppStartIntegration', () { late Fixture fixture; @@ -53,7 +44,7 @@ void main() { test('native app start measurement added to first transaction', () async { fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - await registerIntegration(fixture); + await fixture.registerIntegration(); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); @@ -70,7 +61,7 @@ void main() { () async { fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - await registerIntegration(fixture); + await fixture.registerIntegration(); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); @@ -88,7 +79,7 @@ void main() { fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); final measurement = SentryMeasurement.warmAppStart(Duration(seconds: 1)); - await registerIntegration(fixture); + await fixture.registerIntegration(); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer).copyWith(); transaction.measurements[measurement.name] = measurement; @@ -107,7 +98,7 @@ void main() { test('native app start measurement not added if more than 60s', () async { fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(60001); - await registerIntegration(fixture); + await fixture.registerIntegration(); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); @@ -122,8 +113,9 @@ void main() { () async { fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - await registerIntegration(fixture); - final appStartInfo = await fixture.sut.getAppStartInfo(); + await fixture.registerIntegration(); + final appStartInfo = fixture.sut.appStartInfo; + expect(appStartInfo?.start, DateTime.fromMillisecondsSinceEpoch(0)); expect(appStartInfo?.end, DateTime.fromMillisecondsSinceEpoch(10)); }); @@ -133,7 +125,7 @@ void main() { () async { fixture.options.autoAppStart = false; - await registerIntegration(fixture); + await fixture.registerIntegration(); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); @@ -149,14 +141,12 @@ void main() { test( 'does not trigger timeout if autoAppStart is false and setAppStartEnd is not called', () async { - // setting a frame callback with a bigger timeout than our app start timeout so the timeout would theoretically be triggered - fixture = Fixture( - frameCallbackTimeout: NativeAppStartIntegration.timeoutDuration + - const Duration(seconds: 5)); fixture.options.addIntegration(fixture.sut); fixture.options.autoAppStart = false; - await registerIntegration(fixture); + fixture.sut.call(fixture.hub, fixture.options); + + // await fixture.registerIntegration(); final tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); @@ -175,7 +165,7 @@ void main() { () async { fixture.options.autoAppStart = false; - await registerIntegration(fixture); + await fixture.registerIntegration(); fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); // SentryFlutter.setAppStartEnd(DateTime.fromMillisecondsSinceEpoch(10)); @@ -191,7 +181,7 @@ void main() { expect(measurement.value, 10); expect(measurement.unit, DurationSentryMeasurementUnit.milliSecond); - final appStartInfo = await fixture.sut.getAppStartInfo(); + final appStartInfo = fixture.sut.appStartInfo; final appStartSpan = enriched.spans.firstWhereOrNull((element) => element.context.description == appStartInfo!.appStartTypeDescription); @@ -271,14 +261,15 @@ void main() { when(fixture.binding.fetchNativeAppStart()) .thenAnswer((_) async => appStartInfoSrc); - await registerIntegration(fixture); + await fixture.registerIntegration(); + final processor = fixture.options.eventProcessors.first; tracer = fixture.createTracer(); final transaction = SentryTransaction(tracer); enriched = await processor.apply(transaction, Hint()) as SentryTransaction; - final appStartInfo = await fixture.sut.getAppStartInfo(); + final appStartInfo = fixture.sut.appStartInfo; coldStartSpan = enriched.spans.firstWhereOrNull((element) => element.context.description == appStartInfo?.appStartTypeDescription); @@ -401,22 +392,28 @@ void main() { }); } -class Fixture extends IntegrationTestFixture { - @override - MockHub get hub => super.hub as MockHub; - - Fixture({Duration? frameCallbackTimeout}) - : super((binding) => NativeAppStartIntegration( - binding, - FakeFrameCallbackHandler( - finishAfterDuration: frameCallbackTimeout ?? - const Duration(milliseconds: 50)))) { - hub = MockHub(); - // ignore: invalid_use_of_internal_member +class Fixture { + final options = SentryFlutterOptions(dsn: fakeDsn); + final binding = MockSentryNativeBinding(); + final callbackHandler = MockFrameCallbackHandler(); + final hub = MockHub(); + + late NativeAppStartIntegration sut = NativeAppStartIntegration( + binding, + callbackHandler, + hub: hub, + ); + + Fixture() { when(hub.options).thenReturn(options); SentryFlutter.sentrySetupStartTime = DateTime.now().toUtc(); } + Future registerIntegration() async { + sut.call(hub, options); + callbackHandler.postFrameCallback!(Duration(milliseconds: 0)); + } + // ignore: invalid_use_of_internal_member SentryTracer createTracer({ bool? sampled = true, @@ -426,6 +423,7 @@ class Fixture extends IntegrationTestFixture { 'op', samplingDecision: SentryTracesSamplingDecision(sampled!), ); - return SentryTracer(context, hub); + return SentryTracer(context, hub, + startTimestamp: DateTime.fromMillisecondsSinceEpoch(0)); } } diff --git a/flutter/test/mock_frame_callback_handler.dart b/flutter/test/mock_frame_callback_handler.dart new file mode 100644 index 0000000000..ef09f63e98 --- /dev/null +++ b/flutter/test/mock_frame_callback_handler.dart @@ -0,0 +1,25 @@ +import 'package:flutter/scheduler.dart'; +import 'package:sentry_flutter/src/frame_callback_handler.dart'; + +import 'mocks.dart'; + +class MockFrameCallbackHandler implements FrameCallbackHandler { + FrameCallback? postFrameCallback; + FrameCallback? persistentFrameCallback; + + @override + void addPostFrameCallback(FrameCallback callback) { + this.postFrameCallback = callback; + } + + @override + void addPersistentFrameCallback(FrameCallback callback) { + this.persistentFrameCallback = persistentFrameCallback; + } + + @override + bool hasScheduledFrame = true; + + @override + Future get endOfFrame => Future.value(); +} From a7fd92d1b66388d8a1e0966e35878cfd76bfd3ef Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 9 Sep 2024 14:43:36 +0200 Subject: [PATCH 04/20] Move `NativeAppStartEventProcessor` functionality into integration --- .../native_app_start_event_processor.dart | 309 ------------- .../native_app_start_handler.dart | 294 ++++++++++++ .../native_app_start_integration.dart | 191 +------- .../navigation/sentry_navigator_observer.dart | 22 +- .../navigation/time_to_display_tracker.dart | 9 - flutter/lib/src/sentry_flutter.dart | 16 +- .../native_app_start_handler_test.dart | 348 ++++++++++++++ .../native_app_start_integration_test.dart | 426 ++---------------- flutter/test/mock_frame_callback_handler.dart | 7 +- .../time_to_display_tracker_test.dart | 42 -- .../test/sentry_navigator_observer_test.dart | 11 - 11 files changed, 708 insertions(+), 967 deletions(-) delete mode 100644 flutter/lib/src/event_processor/native_app_start_event_processor.dart create mode 100644 flutter/lib/src/integrations/native_app_start_handler.dart create mode 100644 flutter/test/integrations/native_app_start_handler_test.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 deleted file mode 100644 index b7c4f7d54f..0000000000 --- a/flutter/lib/src/event_processor/native_app_start_event_processor.dart +++ /dev/null @@ -1,309 +0,0 @@ -// ignore_for_file: invalid_use_of_internal_member - -import 'dart:async'; - -import '../../sentry_flutter.dart'; -import '../integrations/integrations.dart'; - -// ignore: implementation_imports -import 'package:sentry/src/sentry_tracer.dart'; - -/// EventProcessor that enriches [SentryTransaction] objects with app start -/// measurement. -class NativeAppStartEventProcessor implements EventProcessor { - final Hub _hub; - - NativeAppStartEventProcessor({Hub? hub}) : _hub = hub ?? HubAdapter(); - - @override - Future apply(SentryEvent event, Hint hint) async { - final options = _hub.options; - - final integrations = - options.integrations.whereType(); - if (integrations.isEmpty) { - return event; - } - final nativeAppStartIntegration = integrations.first; - - if (event is! SentryTransaction || options is! SentryFlutterOptions) { - return event; - } - - AppStartInfo? appStartInfo = nativeAppStartIntegration.appStartInfo; - if (appStartInfo == null) { - return event; - } - - if (!options.autoAppStart) { - final appStartEnd = nativeAppStartIntegration.appStartEnd; - if (appStartEnd != null) { - appStartInfo.end = appStartEnd; - } else { - // If autoAppStart is disabled and appStartEnd is not set, we can't add app starts - return event; - } - } - - final measurement = appStartInfo.toMeasurement(); - if (measurement != null) { - event.measurements[measurement.name] = measurement; - } - - await _attachAppStartSpans(appStartInfo, event.tracer); - - return event; - } - - Future _attachAppStartSpans( - AppStartInfo appStartInfo, SentryTracer transaction) async { - final transactionTraceId = transaction.context.traceId; - final appStartEnd = appStartInfo.end; - if (appStartEnd == null) { - return; - } - - final appStartSpan = await _createAndFinishSpan( - tracer: transaction, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.appStartTypeDescription, - parentSpanId: transaction.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.start, - endTimestamp: appStartEnd); - - await _attachNativeSpans(appStartInfo, transaction, appStartSpan); - - final pluginRegistrationSpan = await _createAndFinishSpan( - tracer: transaction, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.pluginRegistrationDescription, - parentSpanId: appStartSpan.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.start, - endTimestamp: appStartInfo.pluginRegistration); - - final sentrySetupSpan = await _createAndFinishSpan( - tracer: transaction, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.sentrySetupDescription, - parentSpanId: appStartSpan.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.pluginRegistration, - endTimestamp: appStartInfo.sentrySetupStart); - - final firstFrameRenderSpan = await _createAndFinishSpan( - tracer: transaction, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.firstFrameRenderDescription, - parentSpanId: appStartSpan.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.sentrySetupStart, - endTimestamp: appStartEnd); - - transaction.children.addAll([ - appStartSpan, - pluginRegistrationSpan, - sentrySetupSpan, - firstFrameRenderSpan - ]); - } - - Future _attachNativeSpans(AppStartInfo appStartInfo, - SentryTracer transaction, SentrySpan parent) async { - await Future.forEach(appStartInfo.nativeSpanTimes, - (timeSpan) async { - try { - final span = await _createAndFinishSpan( - tracer: transaction, - operation: appStartInfo.appStartTypeOperation, - description: timeSpan.description, - parentSpanId: parent.context.spanId, - traceId: transaction.context.traceId, - startTimestamp: timeSpan.start, - endTimestamp: timeSpan.end); - span.data.putIfAbsent('native', () => true); - transaction.children.add(span); - } catch (e) { - _hub.options.logger(SentryLevel.warning, - 'Failed to attach native span to app start transaction: $e'); - } - }); - } - - Future _createAndFinishSpan({ - required SentryTracer tracer, - required String operation, - required String description, - required SpanId parentSpanId, - required SentryId traceId, - required DateTime startTimestamp, - required DateTime endTimestamp, - }) async { - final span = SentrySpan( - tracer, - SentrySpanContext( - operation: operation, - description: description, - parentSpanId: parentSpanId, - traceId: traceId, - ), - _hub, - startTimestamp: startTimestamp); - await span.finish(endTimestamp: endTimestamp); - return span; - } -} - -class NativeAppStartHandler { - final Hub _hub; - - NativeAppStartHandler({Hub? hub}) : _hub = hub ?? HubAdapter(); - - Future call( - AppStartInfo appStartInfo, - SentryFlutterOptions options, - ) async { - const screenName = SentryNavigatorObserver.rootScreenName; - final transaction = _hub.startTransaction( - screenName, - SentrySpanOperations.uiLoad, - startTimestamp: appStartInfo.start, - ); - final ttidSpan = transaction.startChild( - SentrySpanOperations.uiTimeToInitialDisplay, - description: '$screenName initial display', - startTimestamp: appStartInfo.start, - ); - await ttidSpan.finish(endTimestamp: appStartInfo.end); - await transaction.finish(endTimestamp: appStartInfo.end); - - if (!options.autoAppStart) { - final appStartEnd = SentryFlutter.appStatEnd; - if (appStartEnd != null) { - appStartInfo.end = appStartEnd; - } else { - // If autoAppStart is disabled and appStartEnd is not set, we can't add app starts - return; - } - } - - await attachMeasurements(transaction as SentryTransaction, appStartInfo); - await attachSpans(transaction as SentryTransaction, appStartInfo); - } - - Future attachMeasurements( - SentryTransaction transaction, AppStartInfo appStartInfo) async { - final measurement = appStartInfo.toMeasurement(); - if (measurement != null) { - transaction.measurements[measurement.name] = measurement; - } - } - - Future attachSpans( - SentryTransaction transaction, AppStartInfo appStartInfo) async { - SentryTracer tracer = transaction.tracer; - - final transactionTraceId = tracer.context.traceId; - final appStartEnd = appStartInfo.end; - if (appStartEnd == null) { - return; - } - - final appStartSpan = await _createAndFinishSpan( - tracer: tracer, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.appStartTypeDescription, - parentSpanId: tracer.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.start, - endTimestamp: appStartEnd, - ); - - await _attachNativeSpans(appStartInfo, tracer, appStartSpan); - - final pluginRegistrationSpan = await _createAndFinishSpan( - tracer: tracer, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.pluginRegistrationDescription, - parentSpanId: appStartSpan.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.start, - endTimestamp: appStartInfo.pluginRegistration, - ); - - final sentrySetupSpan = await _createAndFinishSpan( - tracer: tracer, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.sentrySetupDescription, - parentSpanId: appStartSpan.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.pluginRegistration, - endTimestamp: appStartInfo.sentrySetupStart, - ); - - final firstFrameRenderSpan = await _createAndFinishSpan( - tracer: tracer, - operation: appStartInfo.appStartTypeOperation, - description: appStartInfo.firstFrameRenderDescription, - parentSpanId: appStartSpan.context.spanId, - traceId: transactionTraceId, - startTimestamp: appStartInfo.sentrySetupStart, - endTimestamp: appStartEnd, - ); - - tracer.children.addAll([ - appStartSpan, - pluginRegistrationSpan, - sentrySetupSpan, - firstFrameRenderSpan, - ]); - } - - Future _attachNativeSpans(AppStartInfo appStartInfo, - SentryTracer transaction, SentrySpan parent) async { - await Future.forEach(appStartInfo.nativeSpanTimes, - (timeSpan) async { - try { - final span = await _createAndFinishSpan( - tracer: transaction, - operation: appStartInfo.appStartTypeOperation, - description: timeSpan.description, - parentSpanId: parent.context.spanId, - traceId: transaction.context.traceId, - startTimestamp: timeSpan.start, - endTimestamp: timeSpan.end, - ); - span.data.putIfAbsent('native', () => true); - transaction.children.add(span); - } catch (e) { - _hub.options.logger(SentryLevel.warning, - 'Failed to attach native span to app start transaction: $e'); - } - }); - } - - Future _createAndFinishSpan({ - required SentryTracer tracer, - required String operation, - required String description, - required SpanId parentSpanId, - required SentryId traceId, - required DateTime startTimestamp, - required DateTime endTimestamp, - }) async { - final span = SentrySpan( - tracer, - SentrySpanContext( - operation: operation, - description: description, - parentSpanId: parentSpanId, - traceId: traceId, - ), - _hub, - startTimestamp: startTimestamp, - ); - await span.finish(endTimestamp: endTimestamp); - return span; - } -} diff --git a/flutter/lib/src/integrations/native_app_start_handler.dart b/flutter/lib/src/integrations/native_app_start_handler.dart new file mode 100644 index 0000000000..4392148cf0 --- /dev/null +++ b/flutter/lib/src/integrations/native_app_start_handler.dart @@ -0,0 +1,294 @@ +import '../../sentry_flutter.dart'; +import '../native/native_app_start.dart'; +import '../native/sentry_native_binding.dart'; + +// ignore: implementation_imports +import 'package:sentry/src/sentry_tracer.dart'; + +/// Handles communication with native frameworks in order to enrich +/// root [SentryTransaction] with app start data for mobile vitals. +class NativeAppStartHandler { + NativeAppStartHandler(this._native, {Hub? hub}) : _hub = hub ?? HubAdapter(); + + final SentryNativeBinding _native; + final Hub _hub; + + /// We filter out App starts more than 60s + static const _maxAppStartMillis = 60000; + + Future call({required DateTime? appStartEnd}) async { + final nativeAppStart = await _native.fetchNativeAppStart(); + if (nativeAppStart == null) { + return; + } + final appStartInfo = _infoNativeAppStart(nativeAppStart, appStartEnd); + if (appStartInfo == null) { + return; + } + + final flutterOptions = _hub.options as SentryFlutterOptions; + + // Create Transaction & Span + + const screenName = SentryNavigatorObserver.rootScreenName; + final transaction = _hub.startTransaction( + screenName, + SentrySpanOperations.uiLoad, + startTimestamp: appStartInfo.start, + ); + final ttidSpan = transaction.startChild( + SentrySpanOperations.uiTimeToInitialDisplay, + description: '$screenName initial display', + startTimestamp: appStartInfo.start, + ); + + // Enrich Transaction + + final sentryTracer = transaction as SentryTracer; + + SentryMeasurement? measurement; + if (flutterOptions.autoAppStart) { + measurement = appStartInfo.toMeasurement(); + } else if (appStartEnd != null) { + appStartInfo.end = appStartEnd; + measurement = appStartInfo.toMeasurement(); + } + + if (measurement != null) { + sentryTracer.measurements[measurement.name] = measurement; + await _attachAppStartSpans(appStartInfo, sentryTracer); + } + + // Finish Transaction & Span + + await ttidSpan.finish(endTimestamp: appStartInfo.end); + await transaction.finish(endTimestamp: appStartInfo.end); + } + + AppStartInfo? _infoNativeAppStart( + NativeAppStart nativeAppStart, + DateTime? appStartEnd, + ) { + final sentrySetupStartDateTime = SentryFlutter.sentrySetupStartTime; + if (sentrySetupStartDateTime == null) { + return null; + } + + final flutterOptions = _hub.options as SentryFlutterOptions; + + final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.appStartTime.toInt()); + final pluginRegistrationDateTime = DateTime.fromMillisecondsSinceEpoch( + nativeAppStart.pluginRegistrationTime); + + if (flutterOptions.autoAppStart) { + // We only assign the current time if it's not already set - this is useful in tests + appStartEnd ??= flutterOptions.clock(); + + final duration = appStartEnd.difference(appStartDateTime); + + // 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 (duration.inMilliseconds > _maxAppStartMillis) { + return null; + } + } + + List nativeSpanTimes = []; + for (final entry in nativeAppStart.nativeSpanTimes.entries) { + try { + final startTimestampMs = + entry.value['startTimestampMsSinceEpoch'] as int; + final endTimestampMs = entry.value['stopTimestampMsSinceEpoch'] as int; + nativeSpanTimes.add(TimeSpan( + start: DateTime.fromMillisecondsSinceEpoch(startTimestampMs), + end: DateTime.fromMillisecondsSinceEpoch(endTimestampMs), + description: entry.key as String, + )); + } catch (e) { + _hub.options.logger( + SentryLevel.warning, 'Failed to parse native span times: $e'); + continue; + } + } + + // We want to sort because the native spans are not guaranteed to be in order. + // Performance wise this won't affect us since the native span amount is very low. + nativeSpanTimes.sort((a, b) => a.start.compareTo(b.start)); + + return AppStartInfo( + nativeAppStart.isColdStart ? AppStartType.cold : AppStartType.warm, + start: appStartDateTime, + end: appStartEnd, + pluginRegistration: pluginRegistrationDateTime, + sentrySetupStart: sentrySetupStartDateTime, + nativeSpanTimes: nativeSpanTimes, + ); + } + + Future _attachAppStartSpans( + AppStartInfo appStartInfo, SentryTracer transaction) async { + final transactionTraceId = transaction.context.traceId; + final appStartEnd = appStartInfo.end; + if (appStartEnd == null) { + return; + } + + final appStartSpan = await _createAndFinishSpan( + tracer: transaction, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.appStartTypeDescription, + parentSpanId: transaction.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.start, + endTimestamp: appStartEnd, + ); + + await _attachNativeSpans(appStartInfo, transaction, appStartSpan); + + final pluginRegistrationSpan = await _createAndFinishSpan( + tracer: transaction, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.pluginRegistrationDescription, + parentSpanId: appStartSpan.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.start, + endTimestamp: appStartInfo.pluginRegistration, + ); + + final sentrySetupSpan = await _createAndFinishSpan( + tracer: transaction, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.sentrySetupDescription, + parentSpanId: appStartSpan.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.pluginRegistration, + endTimestamp: appStartInfo.sentrySetupStart, + ); + + final firstFrameRenderSpan = await _createAndFinishSpan( + tracer: transaction, + operation: appStartInfo.appStartTypeOperation, + description: appStartInfo.firstFrameRenderDescription, + parentSpanId: appStartSpan.context.spanId, + traceId: transactionTraceId, + startTimestamp: appStartInfo.sentrySetupStart, + endTimestamp: appStartEnd, + ); + + transaction.children.addAll([ + appStartSpan, + pluginRegistrationSpan, + sentrySetupSpan, + firstFrameRenderSpan + ]); + } + + Future _attachNativeSpans( + AppStartInfo appStartInfo, + SentryTracer transaction, + SentrySpan parent, + ) async { + await Future.forEach(appStartInfo.nativeSpanTimes, + (timeSpan) async { + try { + final span = await _createAndFinishSpan( + tracer: transaction, + operation: appStartInfo.appStartTypeOperation, + description: timeSpan.description, + parentSpanId: parent.context.spanId, + traceId: transaction.context.traceId, + startTimestamp: timeSpan.start, + endTimestamp: timeSpan.end, + ); + span.data.putIfAbsent('native', () => true); + transaction.children.add(span); + } catch (e) { + _hub.options.logger(SentryLevel.warning, + 'Failed to attach native span to app start transaction: $e'); + } + }); + } + + Future _createAndFinishSpan({ + required SentryTracer tracer, + required String operation, + required String description, + required SpanId parentSpanId, + required SentryId traceId, + required DateTime startTimestamp, + required DateTime endTimestamp, + }) async { + final span = SentrySpan( + tracer, + SentrySpanContext( + operation: operation, + description: description, + parentSpanId: parentSpanId, + traceId: traceId, + ), + _hub, + startTimestamp: startTimestamp, + ); + await span.finish(endTimestamp: endTimestamp); + return span; + } +} + +enum AppStartType { cold, warm } + +class AppStartInfo { + AppStartInfo( + this.type, { + required this.start, + required this.pluginRegistration, + required this.sentrySetupStart, + required this.nativeSpanTimes, + this.end, + }); + + final AppStartType type; + final DateTime start; + final List nativeSpanTimes; + + // We allow the end to be null, since it might be set at a later time + // with setAppStartEnd when autoAppStart is disabled + DateTime? end; + + final DateTime pluginRegistration; + final DateTime sentrySetupStart; + + Duration? get duration => end?.difference(start); + + SentryMeasurement? toMeasurement() { + final duration = this.duration; + if (duration == null) { + return null; + } + return type == AppStartType.cold + ? SentryMeasurement.coldAppStart(duration) + : SentryMeasurement.warmAppStart(duration); + } + + String get appStartTypeOperation => 'app.start.${type.name}'; + + String get appStartTypeDescription => + type == AppStartType.cold ? 'Cold Start' : 'Warm Start'; + final pluginRegistrationDescription = 'App start to plugin registration'; + final sentrySetupDescription = 'Before Sentry Init Setup'; + final firstFrameRenderDescription = 'First frame render'; +} + +class TimeSpan { + TimeSpan({required this.start, required this.end, required this.description}); + + final DateTime start; + final DateTime end; + final String description; +} diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 1a99eea87b..7f7045308f 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -1,22 +1,17 @@ -// ignore_for_file: invalid_use_of_internal_member import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; import '../frame_callback_handler.dart'; -import '../native/native_app_start.dart'; -import '../native/sentry_native_binding.dart'; -import '../event_processor/native_app_start_event_processor.dart'; +import 'native_app_start_handler.dart'; -/// Integration which handles communication with native frameworks in order to -/// enrich [SentryTransaction] objects with app start data for mobile vitals. +/// Integration which calls [NativeAppStartHandler] after +/// [SchedulerBinding.instance.addPostFrameCallback] is called. class NativeAppStartIntegration extends Integration { - NativeAppStartIntegration(this._native, this._frameCallbackHandler, - {Hub? hub}) - : _hub = hub ?? HubAdapter(); + NativeAppStartIntegration( + this._frameCallbackHandler, this._nativeAppStartHandler); - final SentryNativeBinding _native; final FrameCallbackHandler _frameCallbackHandler; - final Hub _hub; + final NativeAppStartHandler _nativeAppStartHandler; /// This timestamp marks the end of app startup. Either set automatically when /// [SentryFlutterOptions.autoAppStart] is true, or by calling @@ -24,183 +19,11 @@ class NativeAppStartIntegration extends Integration { @internal DateTime? appStartEnd; - /// Flag indicating if app start measurement was added to the first transaction. - @internal - bool didAddAppStartMeasurement = false; - - /// Timeout duration to wait for the app start info to be fetched. - static const _timeoutDuration = Duration(seconds: 10); - - @visibleForTesting - static Duration get timeoutDuration => _timeoutDuration; - - /// We filter out App starts more than 60s - static const _maxAppStartMillis = 60000; - - AppStartInfo? _appStartInfo; - - @internal - static bool isIntegrationTest = false; - - @internal - AppStartInfo? get appStartInfo => _appStartInfo; - @override void call(Hub hub, SentryFlutterOptions options) { - if (isIntegrationTest) { - final appStartInfo = AppStartInfo( - AppStartType.cold, - start: DateTime.now(), - end: DateTime.now().add(const Duration(milliseconds: 100)), - pluginRegistration: - DateTime.now().add(const Duration(milliseconds: 50)), - sentrySetupStart: DateTime.now().add(const Duration(milliseconds: 60)), - nativeSpanTimes: [], - ); - _appStartInfo = appStartInfo; - return; - } - _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - final nativeAppStart = await _native.fetchNativeAppStart(); - if (nativeAppStart == null) { - return; - } - final appStartInfo = _infoNativeAppStart(nativeAppStart, options); - if (appStartInfo == null) { - return; - } - _appStartInfo = appStartInfo; - - const screenName = SentryNavigatorObserver.rootScreenName; - final transaction = hub.startTransaction( - screenName, SentrySpanOperations.uiLoad, - startTimestamp: appStartInfo.start); - final ttidSpan = transaction.startChild( - SentrySpanOperations.uiTimeToInitialDisplay, - description: '$screenName initial display', - startTimestamp: appStartInfo.start); - await ttidSpan.finish(endTimestamp: appStartInfo.end); - await transaction.finish(endTimestamp: appStartInfo.end); + await _nativeAppStartHandler.call(appStartEnd: appStartEnd); }); - - options.addEventProcessor(NativeAppStartEventProcessor(hub: hub)); - options.sdk.addIntegration('nativeAppStartIntegration'); } - - AppStartInfo? _infoNativeAppStart( - NativeAppStart nativeAppStart, SentryFlutterOptions options) { - final sentrySetupStartDateTime = SentryFlutter.sentrySetupStartTime; - if (sentrySetupStartDateTime == null) { - return null; - } - - final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.appStartTime.toInt()); - final pluginRegistrationDateTime = DateTime.fromMillisecondsSinceEpoch( - nativeAppStart.pluginRegistrationTime); - - if (options.autoAppStart) { - // We only assign the current time if it's not already set - this is useful in tests - appStartEnd ??= options.clock(); - - final duration = appStartEnd?.difference(appStartDateTime); - - // 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 (duration != null && duration.inMilliseconds > _maxAppStartMillis) { - return null; - } - } - - List nativeSpanTimes = []; - for (final entry in nativeAppStart.nativeSpanTimes.entries) { - try { - final startTimestampMs = - entry.value['startTimestampMsSinceEpoch'] as int; - final endTimestampMs = entry.value['stopTimestampMsSinceEpoch'] as int; - nativeSpanTimes.add(TimeSpan( - start: DateTime.fromMillisecondsSinceEpoch(startTimestampMs), - end: DateTime.fromMillisecondsSinceEpoch(endTimestampMs), - description: entry.key as String, - )); - } catch (e) { - _hub.options.logger( - SentryLevel.warning, 'Failed to parse native span times: $e'); - continue; - } - } - - // We want to sort because the native spans are not guaranteed to be in order. - // Performance wise this won't affect us since the native span amount is very low. - nativeSpanTimes.sort((a, b) => a.start.compareTo(b.start)); - - return AppStartInfo( - nativeAppStart.isColdStart ? AppStartType.cold : AppStartType.warm, - start: appStartDateTime, - end: appStartEnd, - pluginRegistration: pluginRegistrationDateTime, - sentrySetupStart: sentrySetupStartDateTime, - nativeSpanTimes: nativeSpanTimes, - ); - } -} - -enum AppStartType { cold, warm } - -class AppStartInfo { - AppStartInfo( - this.type, { - required this.start, - required this.pluginRegistration, - required this.sentrySetupStart, - required this.nativeSpanTimes, - this.end, - }); - - final AppStartType type; - final DateTime start; - final List nativeSpanTimes; - - // We allow the end to be null, since it might be set at a later time - // with setAppStartEnd when autoAppStart is disabled - DateTime? end; - - final DateTime pluginRegistration; - final DateTime sentrySetupStart; - - Duration? get duration => end?.difference(start); - - SentryMeasurement? toMeasurement() { - final duration = this.duration; - if (duration == null) { - return null; - } - return type == AppStartType.cold - ? SentryMeasurement.coldAppStart(duration) - : SentryMeasurement.warmAppStart(duration); - } - - String get appStartTypeOperation => 'app.start.${type.name}'; - - String get appStartTypeDescription => - type == AppStartType.cold ? 'Cold Start' : 'Warm Start'; - final pluginRegistrationDescription = 'App start to plugin registration'; - final sentrySetupDescription = 'Before Sentry Init Setup'; - final firstFrameRenderDescription = 'First frame render'; -} - -class TimeSpan { - TimeSpan({required this.start, required this.end, required this.description}); - - final DateTime start; - final DateTime end; - final String description; } diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index a2d6adb413..7ec2cf7d1a 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -5,7 +5,6 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; import 'package:meta/meta.dart'; -import '../integrations/integrations.dart'; import '../native/native_frames.dart'; import '../native/sentry_native_binding.dart'; import 'time_to_display_tracker.dart'; @@ -348,16 +347,6 @@ class SentryNavigatorObserver extends RouteObserver> { bool isAppStart = routeName == '/'; DateTime startTimestamp = _hub.options.clock(); - DateTime? endTimestamp; - - // TODO: Handle in app_start_event_processor - // if (isAppStart) { - // final appStartInfo = await NativeAppStartIntegration.getAppStartInfo(); - // if (appStartInfo == null) return; - // - // startTimestamp = appStartInfo.start; - // endTimestamp = appStartInfo.end; - // } await _startTransaction(route, startTimestamp); @@ -366,12 +355,11 @@ class SentryNavigatorObserver extends RouteObserver> { return; } - if (isAppStart && endTimestamp != null) { - await _timeToDisplayTracker?.trackAppStartTTD(transaction, - startTimestamp: startTimestamp, endTimestamp: endTimestamp); - } else { - await _timeToDisplayTracker?.trackRegularRouteTTD(transaction, - startTimestamp: startTimestamp); + if (!isAppStart) { + await _timeToDisplayTracker?.trackRegularRouteTTD( + transaction, + startTimestamp: startTimestamp, + ); } } catch (exception, stacktrace) { _hub.options.logger( diff --git a/flutter/lib/src/navigation/time_to_display_tracker.dart b/flutter/lib/src/navigation/time_to_display_tracker.dart index 342e305c75..a2c1813317 100644 --- a/flutter/lib/src/navigation/time_to_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_display_tracker.dart @@ -21,15 +21,6 @@ class TimeToDisplayTracker { ? ttfdTracker ?? TimeToFullDisplayTracker() : null; - Future trackAppStartTTD(ISentrySpan transaction, - {required DateTime startTimestamp, - required DateTime endTimestamp}) async { - // We start and immediately finish the spans since we cannot mutate the history of spans. - await _ttidTracker.trackAppStart(transaction, - startTimestamp: startTimestamp, endTimestamp: endTimestamp); - await _trackTTFDIfEnabled(transaction, startTimestamp); - } - Future trackRegularRouteTTD(ISentrySpan transaction, {required DateTime startTimestamp}) async { await _ttidTracker.trackRegularRoute(transaction, startTimestamp); diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 40dbff1663..9a41ab3581 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -16,6 +16,7 @@ import 'flutter_exception_type_identifier.dart'; import 'frame_callback_handler.dart'; import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/integrations.dart'; +import 'integrations/native_app_start_handler.dart'; import 'integrations/screenshot_integration.dart'; import 'native/factory.dart'; import 'native/native_scope_observer.dart'; @@ -205,10 +206,12 @@ mixin SentryFlutter { integrations.add(LoadReleaseIntegration()); if (native != null) { - integrations.add(NativeAppStartIntegration( - native, - DefaultFrameCallbackHandler(), - )); + integrations.add( + NativeAppStartIntegration( + DefaultFrameCallbackHandler(), + NativeAppStartHandler(native), + ), + ); } return integrations; } @@ -229,8 +232,9 @@ mixin SentryFlutter { // ignore: invalid_use_of_internal_member final integrations = Sentry.currentHub.options.integrations .whereType(); - integrations - .forEach((integration) => integration.appStartEnd = appStartEnd); + for (var integration in integrations) { + integration.appStartEnd = appStartEnd; + } } static DateTime? get appStatEnd { diff --git a/flutter/test/integrations/native_app_start_handler_test.dart b/flutter/test/integrations/native_app_start_handler_test.dart new file mode 100644 index 0000000000..e0c7adf706 --- /dev/null +++ b/flutter/test/integrations/native_app_start_handler_test.dart @@ -0,0 +1,348 @@ +@TestOn('vm') +library flutter_test; + +import 'package:collection/collection.dart'; +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/integrations.dart'; +import 'package:sentry_flutter/src/integrations/native_app_start_handler.dart'; +import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart'; +import 'package:sentry/src/sentry_tracer.dart'; +import 'package:sentry_flutter/src/native/native_app_start.dart'; + +import '../mocks.dart'; +import '../mocks.mocks.dart'; + +void main() { + void setupMocks(Fixture fixture) { + when(fixture.hub.startTransaction( + 'root /', + 'ui.load', + description: null, + startTimestamp: anyNamed('startTimestamp'), + )).thenReturn(fixture.tracer); + + when(fixture.hub.configureScope(captureAny)).thenAnswer((_) {}); + when(fixture.hub.captureTransaction( + any, + traceContext: anyNamed('traceContext'), + )).thenAnswer((_) async => SentryId.empty()); + + when(fixture.nativeBinding.fetchNativeAppStart()).thenAnswer( + (_) async => fixture.nativeAppStart, + ); + } + + group('$NativeAppStartIntegration', () { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + setupMocks(fixture); + }); + + test('native app start measurement added to first transaction', () async { + await fixture.call( + appStartEnd: DateTime.fromMillisecondsSinceEpoch(10), + ); + final transaction = fixture.capturedTransaction(); + + final measurement = transaction.measurements['app_start_cold']!; + expect(measurement.value, 10); + expect(measurement.unit, DurationSentryMeasurementUnit.milliSecond); + }); + + test('measurements appended', () async { + await fixture.call( + appStartEnd: DateTime.fromMillisecondsSinceEpoch(10), + ); + + final measurement = SentryMeasurement.warmAppStart(Duration(seconds: 1)); + + final transaction = fixture.capturedTransaction().copyWith(); + transaction.measurements[measurement.name] = measurement; + + expect(transaction.measurements.length, 2); + expect(transaction.measurements.containsKey(measurement.name), true); + }); + + test('native app start measurement not added if more than 60s', () async { + await fixture.call( + appStartEnd: DateTime.fromMillisecondsSinceEpoch(60001), + ); + + verifyNever(fixture.hub.captureTransaction( + captureAny, + traceContext: captureAnyNamed('traceContext'), + )); + }); + + test( + 'autoAppStart is false and appStartEnd is not set does not add app start measurement', + () async { + fixture.options.autoAppStart = false; + await fixture.call( + appStartEnd: null, + ); + + final transaction = fixture.capturedTransaction(); + + expect(transaction.measurements.isEmpty, true); + expect(transaction.spans.length, + 1); // Only containing ui.load.initial_display + expect(transaction.spans[0].context.operation, 'ui.load.initial_display'); + }); + + test( + 'autoAppStart is false and appStartEnd is set adds app start measurement', + () async { + fixture.options.autoAppStart = false; + + await fixture.call( + appStartEnd: DateTime.fromMillisecondsSinceEpoch(10), + ); + + final transaction = fixture.capturedTransaction(); + + final measurement = transaction.measurements['app_start_cold']!; + expect(measurement.value, 10); + expect(measurement.unit, DurationSentryMeasurementUnit.milliSecond); + + final spans = transaction.spans; + + final appStartSpan = spans.firstWhereOrNull( + (element) => element.context.description == 'Cold Start'); + + final pluginRegistrationSpan = spans.firstWhereOrNull((element) => + element.context.description == 'App start to plugin registration'); + + final sentrySetupSpan = spans.firstWhereOrNull((element) => + element.context.description == 'Before Sentry Init Setup'); + + final firstFrameRenderSpan = spans.firstWhereOrNull( + (element) => element.context.description == 'First frame render'); + + expect(appStartSpan, isNotNull); + expect(pluginRegistrationSpan, isNotNull); + expect(sentrySetupSpan, isNotNull); + expect(firstFrameRenderSpan, isNotNull); + }); + }); + + group('App start spans', () { + late SentrySpan? coldStartSpan, + pluginRegistrationSpan, + sentrySetupSpan, + firstFrameRenderSpan; + // ignore: invalid_use_of_internal_member + late SentryTracer tracer; + late Fixture fixture; + late SentryTransaction enriched; + + final validNativeSpanTimes = { + 'correct span description': { + 'startTimestampMsSinceEpoch': 1, + 'stopTimestampMsSinceEpoch': 2, + }, + 'correct span description 2': { + 'startTimestampMsSinceEpoch': 4, + 'stopTimestampMsSinceEpoch': 6, + }, + 'correct span description 3': { + 'startTimestampMsSinceEpoch': 3, + 'stopTimestampMsSinceEpoch': 4, + }, + }; + + final invalidNativeSpanTimes = { + 'failing span with null timestamp': { + 'startTimestampMsSinceEpoch': null, + 'stopTimestampMsSinceEpoch': 3, + }, + 'failing span with string timestamp': { + 'startTimestampMsSinceEpoch': '1', + 'stopTimestampMsSinceEpoch': 3, + }, + }; + + final appStartInfoSrc = NativeAppStart( + appStartTime: 0, + pluginRegistrationTime: 10, + isColdStart: true, + nativeSpanTimes: { + ...validNativeSpanTimes, + ...invalidNativeSpanTimes, + }); + + setUp(() async { + fixture = Fixture(); + tracer = fixture.tracer; + + // dartLoadingEnd needs to be set after engine end (see MockNativeChannel) + SentryFlutter.sentrySetupStartTime = + DateTime.fromMillisecondsSinceEpoch(15); + + setupMocks(fixture); + + when(fixture.nativeBinding.fetchNativeAppStart()) + .thenAnswer((_) async => appStartInfoSrc); + + await fixture.call(appStartEnd: DateTime.fromMillisecondsSinceEpoch(50)); + enriched = fixture.capturedTransaction(); + + final spans = enriched.spans; + + coldStartSpan = spans.firstWhereOrNull( + (element) => element.context.description == 'Cold Start'); + + pluginRegistrationSpan = spans.firstWhereOrNull((element) => + element.context.description == 'App start to plugin registration'); + + sentrySetupSpan = spans.firstWhereOrNull((element) => + element.context.description == 'Before Sentry Init Setup'); + + firstFrameRenderSpan = spans.firstWhereOrNull( + (element) => element.context.description == 'First frame render'); + }); + + test('includes only valid native spans', () async { + final spans = + enriched.spans.where((element) => element.data['native'] == true); + + expect(spans.length, validNativeSpanTimes.length); + + for (final span in spans) { + final validSpan = validNativeSpanTimes[span.context.description]; + expect(validSpan, isNotNull); + expect( + span.startTimestamp, + DateTime.fromMillisecondsSinceEpoch( + validSpan!['startTimestampMsSinceEpoch']!) + .toUtc()); + expect( + span.endTimestamp, + DateTime.fromMillisecondsSinceEpoch( + validSpan['stopTimestampMsSinceEpoch']!) + .toUtc()); + } + }); + + test('are correctly ordered', () async { + final spans = + enriched.spans.where((element) => element.data['native'] == true); + + final orderedSpans = spans.toList() + ..sort((a, b) => a.startTimestamp.compareTo(b.startTimestamp)); + + expect(spans, orderedEquals(orderedSpans)); + }); + + test('ignores invalid spans', () async { + final spans = + enriched.spans.where((element) => element.data['native'] == true); + + expect(spans, isNot(contains('failing span'))); + }); + + test('are added by event processor', () async { + expect(coldStartSpan, isNotNull); + expect(pluginRegistrationSpan, isNotNull); + expect(sentrySetupSpan, isNotNull); + expect(firstFrameRenderSpan, isNotNull); + }); + + test('have correct op', () async { + const op = 'app.start.cold'; + expect(coldStartSpan?.context.operation, op); + expect(pluginRegistrationSpan?.context.operation, op); + expect(sentrySetupSpan?.context.operation, op); + expect(firstFrameRenderSpan?.context.operation, op); + }); + + test('have correct parents', () async { + expect(coldStartSpan?.context.parentSpanId, tracer.context.spanId); + expect(pluginRegistrationSpan?.context.parentSpanId, + coldStartSpan?.context.spanId); + expect( + sentrySetupSpan?.context.parentSpanId, coldStartSpan?.context.spanId); + expect(firstFrameRenderSpan?.context.parentSpanId, + coldStartSpan?.context.spanId); + }); + + test('have correct traceId', () async { + final traceId = tracer.context.traceId; + expect(coldStartSpan?.context.traceId, traceId); + expect(pluginRegistrationSpan?.context.traceId, traceId); + expect(sentrySetupSpan?.context.traceId, traceId); + expect(firstFrameRenderSpan?.context.traceId, traceId); + }); + + test('have correct startTimestamp', () async { + final appStartTime = DateTime.fromMillisecondsSinceEpoch( + appStartInfoSrc.appStartTime.toInt()) + .toUtc(); + expect(coldStartSpan?.startTimestamp, appStartTime); + expect(pluginRegistrationSpan?.startTimestamp, appStartTime); + expect(sentrySetupSpan?.startTimestamp, + pluginRegistrationSpan?.endTimestamp); + expect( + firstFrameRenderSpan?.startTimestamp, sentrySetupSpan?.endTimestamp); + }); + + test('have correct endTimestamp', () async { + final appStartEnd = DateTime.fromMillisecondsSinceEpoch(50); + + final engineReadyEndtime = DateTime.fromMillisecondsSinceEpoch( + appStartInfoSrc.pluginRegistrationTime.toInt()) + .toUtc(); + expect(coldStartSpan?.endTimestamp, appStartEnd.toUtc()); + expect(pluginRegistrationSpan?.endTimestamp, engineReadyEndtime); + expect(sentrySetupSpan?.endTimestamp, + SentryFlutter.sentrySetupStartTime?.toUtc()); + expect(firstFrameRenderSpan?.endTimestamp, coldStartSpan?.endTimestamp); + }); + }); +} + +class Fixture { + final options = SentryFlutterOptions(dsn: fakeDsn); + final nativeBinding = MockSentryNativeBinding(); + final hub = MockHub(); + + late final tracer = SentryTracer( + SentryTransactionContext( + 'name', + 'op', + samplingDecision: SentryTracesSamplingDecision(true), + ), + hub, + startTimestamp: DateTime.fromMillisecondsSinceEpoch(0), + ); + + final nativeAppStart = NativeAppStart( + appStartTime: 0, + pluginRegistrationTime: 10, + isColdStart: true, + nativeSpanTimes: {}, + ); + + late final sut = NativeAppStartHandler(nativeBinding, hub: hub); + + Fixture() { + when(hub.options).thenReturn(options); + SentryFlutter.sentrySetupStartTime = DateTime.now().toUtc(); + } + + Future call({DateTime? appStartEnd}) async { + await sut.call(appStartEnd: appStartEnd); + } + + SentryTransaction capturedTransaction() { + final args = verify(hub.captureTransaction( + captureAny, + traceContext: captureAnyNamed('traceContext'), + )).captured; + return args[0] as SentryTransaction; + } +} diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index 5c18295aea..be60376028 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -1,429 +1,83 @@ @TestOn('vm') library flutter_test; -import 'package:collection/collection.dart'; +import 'dart:core'; + 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/integrations.dart'; +import 'package:sentry_flutter/src/integrations/native_app_start_handler.dart'; import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart'; -import 'package:sentry/src/sentry_tracer.dart'; -import 'package:sentry_flutter/src/native/native_app_start.dart'; import '../mock_frame_callback_handler.dart'; import '../mocks.dart'; import '../mocks.mocks.dart'; void main() { - void setupMocks(Fixture fixture) { - when(fixture.hub.startTransaction('root /', 'ui.load', - description: null, startTimestamp: anyNamed('startTimestamp'))) - .thenReturn(fixture.createTracer()); - when(fixture.hub.configureScope(captureAny)).thenAnswer((_) {}); - when(fixture.hub - .captureTransaction(any, traceContext: anyNamed('traceContext'))) - .thenAnswer((_) async => SentryId.empty()); - } - - group('$NativeAppStartIntegration', () { - late Fixture fixture; - - setUp(() { - fixture = Fixture(); - fixture.options.addIntegration(fixture.sut); - - setupMocks(fixture); - when(fixture.binding.fetchNativeAppStart()).thenAnswer((_) async => - NativeAppStart( - appStartTime: 0, - pluginRegistrationTime: 10, - isColdStart: true, - nativeSpanTimes: {})); - }); - - test('native app start measurement added to first transaction', () async { - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - - await fixture.registerIntegration(); - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - - final processor = fixture.options.eventProcessors.first; - final enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - - final measurement = enriched.measurements['app_start_cold']!; - expect(measurement.value, 10); - expect(measurement.unit, DurationSentryMeasurementUnit.milliSecond); - }); - - test('native app start measurement not added to following transactions', - () async { - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - - await fixture.registerIntegration(); - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - - final processor = fixture.options.eventProcessors.first; - - var enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - var secondEnriched = - await processor.apply(enriched, Hint()) as SentryTransaction; - - expect(secondEnriched.measurements.length, 1); - }); - - test('measurements appended', () async { - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - final measurement = SentryMeasurement.warmAppStart(Duration(seconds: 1)); - - await fixture.registerIntegration(); - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer).copyWith(); - transaction.measurements[measurement.name] = measurement; - - final processor = fixture.options.eventProcessors.first; - - var enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - var secondEnriched = - await processor.apply(enriched, Hint()) as SentryTransaction; - - expect(secondEnriched.measurements.length, 2); - expect(secondEnriched.measurements.containsKey(measurement.name), true); - }); - - test('native app start measurement not added if more than 60s', () async { - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(60001); - - await fixture.registerIntegration(); - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - - final processor = fixture.options.eventProcessors.first; - final enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - - expect(enriched.measurements.isEmpty, true); - }); - - test('native app start integration is called and sets app start info', - () async { - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - - await fixture.registerIntegration(); - final appStartInfo = fixture.sut.appStartInfo; - - expect(appStartInfo?.start, DateTime.fromMillisecondsSinceEpoch(0)); - expect(appStartInfo?.end, DateTime.fromMillisecondsSinceEpoch(10)); - }); - - test( - 'autoAppStart is false and appStartEnd is not set does not add app start measurement', - () async { - fixture.options.autoAppStart = false; - - await fixture.registerIntegration(); - - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - - final processor = fixture.options.eventProcessors.first; - final enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - - expect(enriched.measurements.isEmpty, true); - expect(enriched.spans.isEmpty, true); - }); - - test( - 'does not trigger timeout if autoAppStart is false and setAppStartEnd is not called', - () async { - fixture.options.addIntegration(fixture.sut); - fixture.options.autoAppStart = false; - - fixture.sut.call(fixture.hub, fixture.options); - - // await fixture.registerIntegration(); - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - - final processor = fixture.options.eventProcessors.first; - - final stopwatch = Stopwatch()..start(); - await processor.apply(transaction, Hint()) as SentryTransaction; - stopwatch.stop(); + late Fixture fixture; - expect(stopwatch.elapsed < NativeAppStartIntegration.timeoutDuration, - isTrue); - }); - - test( - 'autoAppStart is false and appStartEnd is set adds app start measurement', - () async { - fixture.options.autoAppStart = false; - - await fixture.registerIntegration(); - - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(10); - // SentryFlutter.setAppStartEnd(DateTime.fromMillisecondsSinceEpoch(10)); - - final tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - - final processor = fixture.options.eventProcessors.first; - final enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - - final measurement = enriched.measurements['app_start_cold']!; - expect(measurement.value, 10); - expect(measurement.unit, DurationSentryMeasurementUnit.milliSecond); - - final appStartInfo = fixture.sut.appStartInfo; - - final appStartSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == appStartInfo!.appStartTypeDescription); - final pluginRegistrationSpan = enriched.spans.firstWhereOrNull( - (element) => - element.context.description == - appStartInfo!.pluginRegistrationDescription); - final sentrySetupSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == appStartInfo!.sentrySetupDescription); - final firstFrameRenderSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == - appStartInfo!.firstFrameRenderDescription); - - expect(appStartSpan, isNotNull); - expect(pluginRegistrationSpan, isNotNull); - expect(sentrySetupSpan, isNotNull); - expect(firstFrameRenderSpan, isNotNull); - }); + setUp(() { + fixture = Fixture(); }); - group('App start spans', () { - late SentrySpan? coldStartSpan, - pluginRegistrationSpan, - sentrySetupSpan, - firstFrameRenderSpan; - // ignore: invalid_use_of_internal_member - late SentryTracer tracer; - late Fixture fixture; - late SentryTransaction enriched; - - final validNativeSpanTimes = { - 'correct span description': { - 'startTimestampMsSinceEpoch': 1, - 'stopTimestampMsSinceEpoch': 2, - }, - 'correct span description 2': { - 'startTimestampMsSinceEpoch': 4, - 'stopTimestampMsSinceEpoch': 6, - }, - 'correct span description 3': { - 'startTimestampMsSinceEpoch': 3, - 'stopTimestampMsSinceEpoch': 4, - }, - }; - - final invalidNativeSpanTimes = { - 'failing span with null timestamp': { - 'startTimestampMsSinceEpoch': null, - 'stopTimestampMsSinceEpoch': 3, - }, - 'failing span with string timestamp': { - 'startTimestampMsSinceEpoch': '1', - 'stopTimestampMsSinceEpoch': 3, - }, - }; - - final appStartInfoSrc = NativeAppStart( - appStartTime: 0, - pluginRegistrationTime: 10, - isColdStart: true, - nativeSpanTimes: { - ...validNativeSpanTimes, - ...invalidNativeSpanTimes, - }); - - setUp(() async { - fixture = Fixture(); - fixture.options.addIntegration(fixture.sut); - fixture.sut.appStartEnd = DateTime.fromMillisecondsSinceEpoch(50); - - // dartLoadingEnd needs to be set after engine end (see MockNativeChannel) - SentryFlutter.sentrySetupStartTime = - DateTime.fromMillisecondsSinceEpoch(15); - - setupMocks(fixture); - - when(fixture.binding.fetchNativeAppStart()) - .thenAnswer((_) async => appStartInfoSrc); - - await fixture.registerIntegration(); - - final processor = fixture.options.eventProcessors.first; - tracer = fixture.createTracer(); - final transaction = SentryTransaction(tracer); - enriched = - await processor.apply(transaction, Hint()) as SentryTransaction; + test('$NativeAppStartIntegration adds integration', () async { + fixture.callIntegration(); - final appStartInfo = fixture.sut.appStartInfo; - - coldStartSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == appStartInfo?.appStartTypeDescription); - pluginRegistrationSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == - appStartInfo?.pluginRegistrationDescription); - sentrySetupSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == appStartInfo?.sentrySetupDescription); - firstFrameRenderSpan = enriched.spans.firstWhereOrNull((element) => - element.context.description == - appStartInfo?.firstFrameRenderDescription); - }); - - test('native app start spans not added to following transactions', - () async { - final processor = fixture.options.eventProcessors.first; - - final transaction = SentryTransaction(fixture.createTracer()); - - final secondEnriched = - await processor.apply(transaction, Hint()) as SentryTransaction; - - expect(secondEnriched.spans.length, 0); - }); - - test('includes only valid native spans', () async { - final spans = - enriched.spans.where((element) => element.data['native'] == true); - - expect(spans.length, validNativeSpanTimes.length); - - for (final span in spans) { - final validSpan = validNativeSpanTimes[span.context.description]; - expect(validSpan, isNotNull); - expect( - span.startTimestamp, - DateTime.fromMillisecondsSinceEpoch( - validSpan!['startTimestampMsSinceEpoch']!) - .toUtc()); - expect( - span.endTimestamp, - DateTime.fromMillisecondsSinceEpoch( - validSpan['stopTimestampMsSinceEpoch']!) - .toUtc()); - } - }); - - test('are correctly ordered', () async { - final spans = - enriched.spans.where((element) => element.data['native'] == true); - - final orderedSpans = spans.toList() - ..sort((a, b) => a.startTimestamp.compareTo(b.startTimestamp)); - - expect(spans, orderedEquals(orderedSpans)); - }); - - test('ignores invalid spans', () async { - final spans = - enriched.spans.where((element) => element.data['native'] == true); - - expect(spans, isNot(contains('failing span'))); - }); + expect( + fixture.options.sdk.integrations.contains('nativeAppStartIntegration'), + true); + }); - test('are added by event processor', () async { - expect(coldStartSpan, isNotNull); - expect(pluginRegistrationSpan, isNotNull); - expect(sentrySetupSpan, isNotNull); - expect(firstFrameRenderSpan, isNotNull); - }); + test('$NativeAppStartIntegration adds postFrameCallback', () async { + fixture.callIntegration(); - test('have correct op', () async { - const op = 'app.start.cold'; - expect(coldStartSpan?.context.operation, op); - expect(pluginRegistrationSpan?.context.operation, op); - expect(sentrySetupSpan?.context.operation, op); - expect(firstFrameRenderSpan?.context.operation, op); - }); + expect(fixture.frameCallbackHandler.postFrameCallback, isNotNull); + }); - test('have correct parents', () async { - expect(coldStartSpan?.context.parentSpanId, tracer.context.spanId); - expect(pluginRegistrationSpan?.context.parentSpanId, - coldStartSpan?.context.spanId); - expect( - sentrySetupSpan?.context.parentSpanId, coldStartSpan?.context.spanId); - expect(firstFrameRenderSpan?.context.parentSpanId, - coldStartSpan?.context.spanId); - }); + test( + '$NativeAppStartIntegration postFrameCallback calls nativeAppStartHandler', + () async { + fixture.callIntegration(); - test('have correct traceId', () async { - final traceId = tracer.context.traceId; - expect(coldStartSpan?.context.traceId, traceId); - expect(pluginRegistrationSpan?.context.traceId, traceId); - expect(sentrySetupSpan?.context.traceId, traceId); - expect(firstFrameRenderSpan?.context.traceId, traceId); - }); + fixture.sut.appStartEnd = DateTime.fromMicrosecondsSinceEpoch(50); - test('have correct startTimestamp', () async { - final appStartTime = DateTime.fromMillisecondsSinceEpoch( - appStartInfoSrc.appStartTime.toInt()) - .toUtc(); - expect(coldStartSpan?.startTimestamp, appStartTime); - expect(pluginRegistrationSpan?.startTimestamp, appStartTime); - expect(sentrySetupSpan?.startTimestamp, - pluginRegistrationSpan?.endTimestamp); - expect( - firstFrameRenderSpan?.startTimestamp, sentrySetupSpan?.endTimestamp); - }); + final postFrameCallback = fixture.frameCallbackHandler.postFrameCallback!; + postFrameCallback(Duration(seconds: 0)); - test('have correct endTimestamp', () async { - final engineReadyEndtime = DateTime.fromMillisecondsSinceEpoch( - appStartInfoSrc.pluginRegistrationTime.toInt()) - .toUtc(); - expect(coldStartSpan?.endTimestamp, fixture.sut.appStartEnd?.toUtc()); - expect(pluginRegistrationSpan?.endTimestamp, engineReadyEndtime); - expect(sentrySetupSpan?.endTimestamp, - SentryFlutter.sentrySetupStartTime?.toUtc()); - expect(firstFrameRenderSpan?.endTimestamp, coldStartSpan?.endTimestamp); - }); + expect(fixture.nativeAppStartHandler.calls, 1); + expect(fixture.nativeAppStartHandler.appStartEnd, fixture.sut.appStartEnd); }); } class Fixture { final options = SentryFlutterOptions(dsn: fakeDsn); - final binding = MockSentryNativeBinding(); - final callbackHandler = MockFrameCallbackHandler(); final hub = MockHub(); + final frameCallbackHandler = MockFrameCallbackHandler(); + final nativeAppStartHandler = MockNativeAppStartHandler(); + late NativeAppStartIntegration sut = NativeAppStartIntegration( - binding, - callbackHandler, - hub: hub, + frameCallbackHandler, + nativeAppStartHandler, ); Fixture() { when(hub.options).thenReturn(options); - SentryFlutter.sentrySetupStartTime = DateTime.now().toUtc(); } - Future registerIntegration() async { + void callIntegration() { sut.call(hub, options); - callbackHandler.postFrameCallback!(Duration(milliseconds: 0)); } +} + +class MockNativeAppStartHandler implements NativeAppStartHandler { + DateTime? appStartEnd; + var calls = 0; - // ignore: invalid_use_of_internal_member - SentryTracer createTracer({ - bool? sampled = true, - }) { - final context = SentryTransactionContext( - 'name', - 'op', - samplingDecision: SentryTracesSamplingDecision(sampled!), - ); - return SentryTracer(context, hub, - startTimestamp: DateTime.fromMillisecondsSinceEpoch(0)); + @override + Future call({required DateTime? appStartEnd}) async { + this.appStartEnd = appStartEnd; + calls += 1; } } diff --git a/flutter/test/mock_frame_callback_handler.dart b/flutter/test/mock_frame_callback_handler.dart index ef09f63e98..d1de20a37a 100644 --- a/flutter/test/mock_frame_callback_handler.dart +++ b/flutter/test/mock_frame_callback_handler.dart @@ -1,7 +1,8 @@ +import 'dart:async'; + import 'package:flutter/scheduler.dart'; import 'package:sentry_flutter/src/frame_callback_handler.dart'; -import 'mocks.dart'; class MockFrameCallbackHandler implements FrameCallbackHandler { FrameCallback? postFrameCallback; @@ -9,12 +10,12 @@ class MockFrameCallbackHandler implements FrameCallbackHandler { @override void addPostFrameCallback(FrameCallback callback) { - this.postFrameCallback = callback; + postFrameCallback = callback; } @override void addPersistentFrameCallback(FrameCallback callback) { - this.persistentFrameCallback = persistentFrameCallback; + persistentFrameCallback = callback; } @override diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index 975adb37fd..9c3e889d05 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -50,22 +50,6 @@ void main() { expect(transaction.context.operation, SentrySpanOperations.uiLoad); expect(transaction.startTimestamp, ttidSpan?.startTimestamp); }); - - test('finishes ttid span', () async { - final sut = fixture.getSut(); - final endTimestamp = - fixture.startTimestamp.add(const Duration(milliseconds: 10)); - - final transaction = fixture.getTransaction(name: '/') as SentryTracer; - await sut.trackAppStartTTD(transaction, - startTimestamp: fixture.startTimestamp, endTimestamp: endTimestamp); - - final ttidSpan = _getTTIDSpan(transaction); - expect(ttidSpan?.context.operation, - SentrySpanOperations.uiTimeToInitialDisplay); - expect(ttidSpan?.finished, isTrue); - expect(ttidSpan?.origin, SentryTraceOrigins.autoUiTimeToDisplay); - }); }); group('in regular routes', () { @@ -166,32 +150,6 @@ void main() { }); }); - group('in root screen app start route', () { - test( - 'finishes span after timeout with deadline exceeded and ttid matching end time', - () async { - final sut = fixture.getSut(); - final transaction = - fixture.getTransaction(name: 'root ("/")') as SentryTracer; - final endTimestamp = - fixture.startTimestamp.add(const Duration(milliseconds: 10)); - - await sut.trackAppStartTTD(transaction, - startTimestamp: fixture.startTimestamp, endTimestamp: endTimestamp); - - final ttidSpan = _getTTIDSpan(transaction); - expect(ttidSpan, isNotNull); - - final ttfdSpan = _getTTFDSpan(transaction); - expect(ttfdSpan, isNotNull); - - expect(ttfdSpan?.finished, isTrue); - expect(ttfdSpan?.status, SpanStatus.deadlineExceeded()); - expect(ttfdSpan?.endTimestamp, ttidSpan?.endTimestamp); - expect(ttfdSpan?.startTimestamp, ttidSpan?.startTimestamp); - }); - }); - test('multiple ttfd timeouts have correct ttid matching end time', () async { final sut = fixture.getSut(); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 9a64d65414..41a7b828f0 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -7,7 +7,6 @@ import 'package:flutter/widgets.dart'; 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/integrations.dart'; import 'package:sentry/src/sentry_tracer.dart'; import 'package:sentry_flutter/src/native/native_frames.dart'; import 'package:sentry_flutter/src/navigation/time_to_display_tracker.dart'; @@ -489,16 +488,6 @@ void main() { test('flutter root name is replaced', () async { final rootRoute = route(RouteSettings(name: '/')); - NativeAppStartIntegration.setAppStartInfo( - AppStartInfo( - AppStartType.cold, - start: DateTime.now().add(const Duration(seconds: 1)), - end: DateTime.now().add(const Duration(seconds: 2)), - pluginRegistration: DateTime.now().add(const Duration(seconds: 3)), - sentrySetupStart: DateTime.now().add(const Duration(seconds: 4)), - nativeSpanTimes: [], - ), - ); final hub = _MockHub(); final span = getMockSentryTracer(name: '/'); From 0256cc176e2643d4fdaae572dcefb43f7982dfcd Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 9 Sep 2024 15:59:35 +0200 Subject: [PATCH 05/20] fix integration test, call handler with corerct hub & options --- .../integration_test/integration_test.dart | 3 -- .../native_app_start_handler.dart | 35 ++++++++++++------- .../native_app_start_integration.dart | 2 +- .../native_app_start_handler_test.dart | 4 +-- flutter/test/mock_frame_callback_handler.dart | 1 - 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/flutter/example/integration_test/integration_test.dart b/flutter/example/integration_test/integration_test.dart index 7a961083b5..89ee5723f4 100644 --- a/flutter/example/integration_test/integration_test.dart +++ b/flutter/example/integration_test/integration_test.dart @@ -8,7 +8,6 @@ import 'package:flutter/widgets.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:http/http.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; -import 'package:sentry_flutter/src/integrations/native_app_start_integration.dart'; import 'package:sentry_flutter_example/main.dart'; void main() { @@ -26,8 +25,6 @@ void main() { // Using fake DSN for testing purposes. Future setupSentryAndApp(WidgetTester tester, {String? dsn, BeforeSendCallback? beforeSendCallback}) async { - NativeAppStartIntegration.isIntegrationTest = true; - await setupSentry( () async { await tester.pumpWidget(SentryScreenshotWidget( diff --git a/flutter/lib/src/integrations/native_app_start_handler.dart b/flutter/lib/src/integrations/native_app_start_handler.dart index 4392148cf0..bb1e0b9ef3 100644 --- a/flutter/lib/src/integrations/native_app_start_handler.dart +++ b/flutter/lib/src/integrations/native_app_start_handler.dart @@ -1,3 +1,5 @@ +// ignore_for_file: invalid_use_of_internal_member + import '../../sentry_flutter.dart'; import '../native/native_app_start.dart'; import '../native/sentry_native_binding.dart'; @@ -8,15 +10,21 @@ import 'package:sentry/src/sentry_tracer.dart'; /// Handles communication with native frameworks in order to enrich /// root [SentryTransaction] with app start data for mobile vitals. class NativeAppStartHandler { - NativeAppStartHandler(this._native, {Hub? hub}) : _hub = hub ?? HubAdapter(); + NativeAppStartHandler(this._native); final SentryNativeBinding _native; - final Hub _hub; + + late final Hub _hub; + late final SentryFlutterOptions _options; /// We filter out App starts more than 60s static const _maxAppStartMillis = 60000; - Future call({required DateTime? appStartEnd}) async { + Future call(Hub hub, SentryFlutterOptions options, + {required DateTime? appStartEnd}) async { + _hub = hub; + _options = options; + final nativeAppStart = await _native.fetchNativeAppStart(); if (nativeAppStart == null) { return; @@ -26,8 +34,6 @@ class NativeAppStartHandler { return; } - final flutterOptions = _hub.options as SentryFlutterOptions; - // Create Transaction & Span const screenName = SentryNavigatorObserver.rootScreenName; @@ -44,10 +50,15 @@ class NativeAppStartHandler { // Enrich Transaction - final sentryTracer = transaction as SentryTracer; + SentryTracer sentryTracer; + if (transaction is SentryTracer) { + sentryTracer = transaction; + } else { + return; + } SentryMeasurement? measurement; - if (flutterOptions.autoAppStart) { + if (options.autoAppStart) { measurement = appStartInfo.toMeasurement(); } else if (appStartEnd != null) { appStartInfo.end = appStartEnd; @@ -74,16 +85,14 @@ class NativeAppStartHandler { return null; } - final flutterOptions = _hub.options as SentryFlutterOptions; - final appStartDateTime = DateTime.fromMillisecondsSinceEpoch( nativeAppStart.appStartTime.toInt()); final pluginRegistrationDateTime = DateTime.fromMillisecondsSinceEpoch( nativeAppStart.pluginRegistrationTime); - if (flutterOptions.autoAppStart) { + if (_options.autoAppStart) { // We only assign the current time if it's not already set - this is useful in tests - appStartEnd ??= flutterOptions.clock(); + appStartEnd ??= _options.clock(); final duration = appStartEnd.difference(appStartDateTime); @@ -112,7 +121,7 @@ class NativeAppStartHandler { description: entry.key as String, )); } catch (e) { - _hub.options.logger( + _options.logger( SentryLevel.warning, 'Failed to parse native span times: $e'); continue; } @@ -210,7 +219,7 @@ class NativeAppStartHandler { span.data.putIfAbsent('native', () => true); transaction.children.add(span); } catch (e) { - _hub.options.logger(SentryLevel.warning, + _options.logger(SentryLevel.warning, 'Failed to attach native span to app start transaction: $e'); } }); diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 7f7045308f..35626390de 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -22,7 +22,7 @@ class NativeAppStartIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) { _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - await _nativeAppStartHandler.call(appStartEnd: appStartEnd); + await _nativeAppStartHandler.call(hub, options, appStartEnd: appStartEnd); }); options.sdk.addIntegration('nativeAppStartIntegration'); } diff --git a/flutter/test/integrations/native_app_start_handler_test.dart b/flutter/test/integrations/native_app_start_handler_test.dart index e0c7adf706..925879ea6a 100644 --- a/flutter/test/integrations/native_app_start_handler_test.dart +++ b/flutter/test/integrations/native_app_start_handler_test.dart @@ -327,7 +327,7 @@ class Fixture { nativeSpanTimes: {}, ); - late final sut = NativeAppStartHandler(nativeBinding, hub: hub); + late final sut = NativeAppStartHandler(nativeBinding); Fixture() { when(hub.options).thenReturn(options); @@ -335,7 +335,7 @@ class Fixture { } Future call({DateTime? appStartEnd}) async { - await sut.call(appStartEnd: appStartEnd); + await sut.call(hub, options, appStartEnd: appStartEnd); } SentryTransaction capturedTransaction() { diff --git a/flutter/test/mock_frame_callback_handler.dart b/flutter/test/mock_frame_callback_handler.dart index d1de20a37a..e57a7c681c 100644 --- a/flutter/test/mock_frame_callback_handler.dart +++ b/flutter/test/mock_frame_callback_handler.dart @@ -3,7 +3,6 @@ import 'dart:async'; import 'package:flutter/scheduler.dart'; import 'package:sentry_flutter/src/frame_callback_handler.dart'; - class MockFrameCallbackHandler implements FrameCallbackHandler { FrameCallback? postFrameCallback; FrameCallback? persistentFrameCallback; From e14455f6c2ff8e700fdacb16a83166083fcb5ba2 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 9 Sep 2024 16:05:12 +0200 Subject: [PATCH 06/20] cleanup --- .../native_app_start_integration_test.dart | 3 +- .../sentry_display_widget_test.dart | 47 ------------------- 2 files changed, 2 insertions(+), 48 deletions(-) diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index be60376028..e76a45043d 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -76,7 +76,8 @@ class MockNativeAppStartHandler implements NativeAppStartHandler { var calls = 0; @override - Future call({required DateTime? appStartEnd}) async { + Future call(Hub hub, SentryFlutterOptions options, + {required DateTime? appStartEnd}) async { this.appStartEnd = appStartEnd; calls += 1; } diff --git a/flutter/test/navigation/sentry_display_widget_test.dart b/flutter/test/navigation/sentry_display_widget_test.dart index a0495bfe8d..6ea39571bb 100644 --- a/flutter/test/navigation/sentry_display_widget_test.dart +++ b/flutter/test/navigation/sentry_display_widget_test.dart @@ -4,7 +4,6 @@ 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/integrations.dart'; import '../fake_frame_callback_handler.dart'; import '../mocks.dart'; @@ -53,52 +52,6 @@ void main() { expect(measurement?.unit, DurationSentryMeasurementUnit.milliSecond); expect(measurement?.value, ttidSpanDuration.inMilliseconds); }); - - testWidgets('SentryDisplayWidget is ignored for app starts', - (WidgetTester tester) async { - final currentRoute = route(RouteSettings(name: '/')); - final appStartInfo = AppStartInfo( - AppStartType.cold, - start: getUtcDateTime().add(Duration(seconds: 1)), - end: getUtcDateTime().add(Duration(seconds: 2)), - pluginRegistration: getUtcDateTime().add(Duration(seconds: 3)), - sentrySetupStart: getUtcDateTime().add(Duration(seconds: 4)), - nativeSpanTimes: [], - ); - NativeAppStartIntegration.setAppStartInfo(appStartInfo); - - 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, 'root / initial display'); - expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); - - expect(ttidSpan.startTimestamp, appStartInfo.start); - expect(ttidSpan.endTimestamp, appStartInfo.end); - final ttidSpanDuration = - ttidSpan.endTimestamp!.difference(ttidSpan.startTimestamp); - - expect(tracer.measurements, hasLength(1)); - final measurement = tracer.measurements['time_to_initial_display']; - expect(measurement, isNotNull); - expect(measurement?.value, appStartInfo.duration?.inMilliseconds); - expect(measurement?.value, ttidSpanDuration.inMilliseconds); - expect(measurement?.unit, DurationSentryMeasurementUnit.milliSecond); - }); } class Fixture { From 52fcc2bd1be327225016927ab74b33823d742ec4 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 9 Sep 2024 16:10:22 +0200 Subject: [PATCH 07/20] add cl entry --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff734baf10..a544c5e05b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +### Enhancements + +- Improve app start integration ([#2266](https://github.com/getsentry/sentry-dart/pull/2266)) + ## 8.9.0 ### Features From f8e4ca34399e8a5eabad62b13a1750316d906218 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Mon, 9 Sep 2024 16:16:21 +0200 Subject: [PATCH 08/20] remove unused getter --- flutter/lib/src/sentry_flutter.dart | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index b2bb9f1bec..06b8d46c82 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -238,18 +238,6 @@ mixin SentryFlutter { } } - static DateTime? get appStatEnd { - // ignore: invalid_use_of_internal_member - final integrations = Sentry.currentHub.options.integrations - .whereType(); - - if (integrations.isNotEmpty) { - return integrations.first.appStartEnd; - } else { - return null; - } - } - static void _setSdk(SentryFlutterOptions options) { // overwrite sdk info with current flutter sdk final sdk = SdkVersion( From 227ffa93c0a9fb83e2b7dfffdf64f55600288a55 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 10 Sep 2024 16:25:11 +0200 Subject: [PATCH 09/20] catch potential errors in native app start integration --- .../integrations/native_app_start_integration.dart | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 35626390de..b601dd2242 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -22,7 +22,16 @@ class NativeAppStartIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) { _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - await _nativeAppStartHandler.call(hub, options, appStartEnd: appStartEnd); + try { + await _nativeAppStartHandler.call(hub, options, appStartEnd: appStartEnd); + } catch (exception, stackTrace) { + options.logger( + SentryLevel.error, + 'Error while capturing native app start', + exception: exception, + stackTrace: stackTrace, + ); + } }); options.sdk.addIntegration('nativeAppStartIntegration'); } From a3211e698c1587515db2af981ee22c4a3b5e6e5a Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 10 Sep 2024 17:09:17 +0200 Subject: [PATCH 10/20] correclty support appStart and and deprecate it --- .../native_app_start_integration.dart | 74 ++++++++++++++----- flutter/lib/src/sentry_flutter.dart | 4 +- .../native_app_start_integration_test.dart | 40 +++++++++- 3 files changed, 98 insertions(+), 20 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index b601dd2242..6d843d61bc 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:meta/meta.dart'; import '../../sentry_flutter.dart'; @@ -12,27 +14,65 @@ class NativeAppStartIntegration extends Integration { final FrameCallbackHandler _frameCallbackHandler; final NativeAppStartHandler _nativeAppStartHandler; + DateTime? _appStartEnd; - /// This timestamp marks the end of app startup. Either set automatically when - /// [SentryFlutterOptions.autoAppStart] is true, or by calling - /// [SentryFlutter.setAppStartEnd] + /// This timestamp marks the end of app startup. Either set by calling + /// [SentryFlutter.setAppStartEnd]. The [SentryFlutterOptions.autoAppStart] + /// option needs to be false. @internal - DateTime? appStartEnd; + set appStartEnd(DateTime appStartEnd) { + _appStartEnd = appStartEnd; + if (!_appStartEndCompleter.isCompleted) { + _appStartEndCompleter.complete(); + } + } + + final Completer _appStartEndCompleter = Completer(); @override - void call(Hub hub, SentryFlutterOptions options) { - _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - try { - await _nativeAppStartHandler.call(hub, options, appStartEnd: appStartEnd); - } catch (exception, stackTrace) { - options.logger( - SentryLevel.error, - 'Error while capturing native app start', - exception: exception, - stackTrace: stackTrace, - ); - } - }); + void call(Hub hub, SentryFlutterOptions options) async { + if (!options.autoAppStart && _appStartEnd == null) { + await callThrowing(options, () async { + await _appStartEndCompleter.future.timeout(const Duration(seconds: 10)); + await _nativeAppStartHandler.call(hub, options, + appStartEnd: _appStartEnd); + }); + } else { + _frameCallbackHandler.addPostFrameCallback((timeStamp) async { + await callThrowing(options, () async { + await _nativeAppStartHandler.call(hub, options, + appStartEnd: _appStartEnd); + }); + }); + } options.sdk.addIntegration('nativeAppStartIntegration'); } + + Future callThrowing( + SentryOptions options, Future Function() callback) async { + try { + await callback(); + } catch (exception, stackTrace) { + options.logger( + SentryLevel.error, + 'Error while capturing native app start', + exception: exception, + stackTrace: stackTrace, + ); + } + } + + Future callHandler(Hub hub, SentryFlutterOptions options) async { + try { + await _nativeAppStartHandler.call(hub, options, + appStartEnd: _appStartEnd); + } catch (exception, stackTrace) { + options.logger( + SentryLevel.error, + 'Error while capturing native app start', + exception: exception, + stackTrace: stackTrace, + ); + } + } } diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 06b8d46c82..955979d2bc 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -228,7 +228,9 @@ mixin SentryFlutter { } /// Manually set when your app finished startup. Make sure to set - /// [SentryFlutterOptions.autoAppStart] to false on init. + /// [SentryFlutterOptions.autoAppStart] to false on init. The timeout duration + /// for this to work is 10 seconds. + @Deprecated('Will be removed in a future release.') static void setAppStartEnd(DateTime appStartEnd) { // ignore: invalid_use_of_internal_member final integrations = Sentry.currentHub.options.integrations diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index e76a45043d..00cee0e5bc 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -40,13 +40,49 @@ void main() { () async { fixture.callIntegration(); - fixture.sut.appStartEnd = DateTime.fromMicrosecondsSinceEpoch(50); + final appStartEnd = DateTime.fromMicrosecondsSinceEpoch(50); + fixture.sut.appStartEnd = appStartEnd; final postFrameCallback = fixture.frameCallbackHandler.postFrameCallback!; postFrameCallback(Duration(seconds: 0)); expect(fixture.nativeAppStartHandler.calls, 1); - expect(fixture.nativeAppStartHandler.appStartEnd, fixture.sut.appStartEnd); + expect(fixture.nativeAppStartHandler.appStartEnd, appStartEnd); + }); + + test( + '$NativeAppStartIntegration with disabled auto app start waits until appStartEnd is set', + () async { + fixture.options.autoAppStart = false; + + fixture.callIntegration(); + + expect(fixture.nativeAppStartHandler.calls, 0); + + final appStartEnd = DateTime.fromMicrosecondsSinceEpoch(50); + fixture.sut.appStartEnd = appStartEnd; + + await Future.delayed(Duration(milliseconds: 10)); + + expect(fixture.frameCallbackHandler.postFrameCallback, isNull); + expect(fixture.nativeAppStartHandler.calls, 1); + expect(fixture.nativeAppStartHandler.appStartEnd, appStartEnd); + }); + + test( + '$NativeAppStartIntegration with disabled auto app start waits until timeout', + () async { + fixture.options.autoAppStart = false; + + fixture.callIntegration(); + + expect(fixture.nativeAppStartHandler.calls, 0); + + await Future.delayed(Duration(seconds: 11)); + + expect(fixture.frameCallbackHandler.postFrameCallback, isNull); + expect(fixture.nativeAppStartHandler.calls, 0); + expect(fixture.nativeAppStartHandler.appStartEnd, null); }); } From 6ae5a6597bc39493c5093f0e644659dbd10ddea9 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 10 Sep 2024 17:20:33 +0200 Subject: [PATCH 11/20] make helper classes private to file --- .../native_app_start_handler.dart | 34 +++++++++---------- .../native_app_start_integration.dart | 3 +- flutter/lib/src/sentry_flutter_options.dart | 1 + 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_handler.dart b/flutter/lib/src/integrations/native_app_start_handler.dart index bb1e0b9ef3..b60bf806f8 100644 --- a/flutter/lib/src/integrations/native_app_start_handler.dart +++ b/flutter/lib/src/integrations/native_app_start_handler.dart @@ -76,7 +76,7 @@ class NativeAppStartHandler { await transaction.finish(endTimestamp: appStartInfo.end); } - AppStartInfo? _infoNativeAppStart( + _AppStartInfo? _infoNativeAppStart( NativeAppStart nativeAppStart, DateTime? appStartEnd, ) { @@ -109,13 +109,13 @@ class NativeAppStartHandler { } } - List nativeSpanTimes = []; + List<_TimeSpan> nativeSpanTimes = []; for (final entry in nativeAppStart.nativeSpanTimes.entries) { try { final startTimestampMs = entry.value['startTimestampMsSinceEpoch'] as int; final endTimestampMs = entry.value['stopTimestampMsSinceEpoch'] as int; - nativeSpanTimes.add(TimeSpan( + nativeSpanTimes.add(_TimeSpan( start: DateTime.fromMillisecondsSinceEpoch(startTimestampMs), end: DateTime.fromMillisecondsSinceEpoch(endTimestampMs), description: entry.key as String, @@ -131,8 +131,8 @@ class NativeAppStartHandler { // Performance wise this won't affect us since the native span amount is very low. nativeSpanTimes.sort((a, b) => a.start.compareTo(b.start)); - return AppStartInfo( - nativeAppStart.isColdStart ? AppStartType.cold : AppStartType.warm, + return _AppStartInfo( + nativeAppStart.isColdStart ? _AppStartType.cold : _AppStartType.warm, start: appStartDateTime, end: appStartEnd, pluginRegistration: pluginRegistrationDateTime, @@ -142,7 +142,7 @@ class NativeAppStartHandler { } Future _attachAppStartSpans( - AppStartInfo appStartInfo, SentryTracer transaction) async { + _AppStartInfo appStartInfo, SentryTracer transaction) async { final transactionTraceId = transaction.context.traceId; final appStartEnd = appStartInfo.end; if (appStartEnd == null) { @@ -200,11 +200,11 @@ class NativeAppStartHandler { } Future _attachNativeSpans( - AppStartInfo appStartInfo, + _AppStartInfo appStartInfo, SentryTracer transaction, SentrySpan parent, ) async { - await Future.forEach(appStartInfo.nativeSpanTimes, + await Future.forEach<_TimeSpan>(appStartInfo.nativeSpanTimes, (timeSpan) async { try { final span = await _createAndFinishSpan( @@ -250,10 +250,10 @@ class NativeAppStartHandler { } } -enum AppStartType { cold, warm } +enum _AppStartType { cold, warm } -class AppStartInfo { - AppStartInfo( +class _AppStartInfo { + _AppStartInfo( this.type, { required this.start, required this.pluginRegistration, @@ -262,9 +262,9 @@ class AppStartInfo { this.end, }); - final AppStartType type; + final _AppStartType type; final DateTime start; - final List nativeSpanTimes; + final List<_TimeSpan> nativeSpanTimes; // We allow the end to be null, since it might be set at a later time // with setAppStartEnd when autoAppStart is disabled @@ -280,7 +280,7 @@ class AppStartInfo { if (duration == null) { return null; } - return type == AppStartType.cold + return type == _AppStartType.cold ? SentryMeasurement.coldAppStart(duration) : SentryMeasurement.warmAppStart(duration); } @@ -288,14 +288,14 @@ class AppStartInfo { String get appStartTypeOperation => 'app.start.${type.name}'; String get appStartTypeDescription => - type == AppStartType.cold ? 'Cold Start' : 'Warm Start'; + type == _AppStartType.cold ? 'Cold Start' : 'Warm Start'; final pluginRegistrationDescription = 'App start to plugin registration'; final sentrySetupDescription = 'Before Sentry Init Setup'; final firstFrameRenderDescription = 'First frame render'; } -class TimeSpan { - TimeSpan({required this.start, required this.end, required this.description}); +class _TimeSpan { + _TimeSpan({required this.start, required this.end, required this.description}); final DateTime start; final DateTime end; diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 6d843d61bc..24f60ea34c 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -15,8 +15,9 @@ class NativeAppStartIntegration extends Integration { final FrameCallbackHandler _frameCallbackHandler; final NativeAppStartHandler _nativeAppStartHandler; DateTime? _appStartEnd; - + /// This timestamp marks the end of app startup. Either set by calling + // ignore: deprecated_member_use_from_same_package /// [SentryFlutter.setAppStartEnd]. The [SentryFlutterOptions.autoAppStart] /// option needs to be false. @internal diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index 18b9d8c919..d8d6582130 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -176,6 +176,7 @@ class SentryFlutterOptions extends SentryOptions { /// Automatically track app start measurement and send it with the /// first transaction. Set to false when configuring option to disable or if /// you want to set the end time of app startup manually using + // ignore: deprecated_member_use_from_same_package /// [SentryFlutter.setAppStartEnd]. bool autoAppStart = true; From 24d43f69f2ee25e6e9c50353a447707be5cf2968 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 10 Sep 2024 17:22:53 +0200 Subject: [PATCH 12/20] remove unused methods --- .../native_app_start_handler.dart | 3 +- .../native_app_start_integration.dart | 2 +- .../time_to_initial_display_tracker.dart | 14 -------- .../time_to_initial_display_tracker_test.dart | 35 ------------------- 4 files changed, 3 insertions(+), 51 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_handler.dart b/flutter/lib/src/integrations/native_app_start_handler.dart index b60bf806f8..b4ca3a9c90 100644 --- a/flutter/lib/src/integrations/native_app_start_handler.dart +++ b/flutter/lib/src/integrations/native_app_start_handler.dart @@ -295,7 +295,8 @@ class _AppStartInfo { } class _TimeSpan { - _TimeSpan({required this.start, required this.end, required this.description}); + _TimeSpan( + {required this.start, required this.end, required this.description}); final DateTime start; final DateTime end; diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index 24f60ea34c..adfbfd200e 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -15,7 +15,7 @@ class NativeAppStartIntegration extends Integration { final FrameCallbackHandler _frameCallbackHandler; final NativeAppStartHandler _nativeAppStartHandler; DateTime? _appStartEnd; - + /// This timestamp marks the end of app startup. Either set by calling // ignore: deprecated_member_use_from_same_package /// [SentryFlutter.setAppStartEnd]. The [SentryFlutterOptions.autoAppStart] 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 ce2f7b9e9c..051d0602b2 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -45,20 +45,6 @@ class TimeToInitialDisplayTracker { ); } - Future trackAppStart(ISentrySpan transaction, - {required DateTime startTimestamp, - required DateTime endTimestamp}) async { - await _trackTimeToInitialDisplay( - transaction: transaction, - startTimestamp: startTimestamp, - endTimestamp: endTimestamp, - origin: SentryTraceOrigins.autoUiTimeToDisplay, - ); - - // Store the end timestamp for potential use by TTFD tracking - _endTimestamp = endTimestamp; - } - Future _trackTimeToInitialDisplay({ required ISentrySpan transaction, required DateTime startTimestamp, 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 6e55029572..f70318376f 100644 --- a/flutter/test/navigation/time_to_initial_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_initial_display_tracker_test.dart @@ -22,41 +22,6 @@ void main() { sut.clear(); }); - group('app start', () { - test('tracking creates and finishes ttid span with correct measurements', - () async { - final endTimestamp = - fixture.startTimestamp.add(const Duration(milliseconds: 10)); - - final transaction = - fixture.getTransaction(name: 'root ("/")') as SentryTracer; - await sut.trackAppStart(transaction, - startTimestamp: fixture.startTimestamp, endTimestamp: endTimestamp); - - 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, 'root ("/") initial display'); - expect(ttidSpan.origin, SentryTraceOrigins.autoUiTimeToDisplay); - expect(ttidSpan.startTimestamp, fixture.startTimestamp); - expect(ttidSpan.endTimestamp, endTimestamp); - - final ttidMeasurement = - transaction.measurements['time_to_initial_display']; - expect(ttidMeasurement, isNotNull); - expect(ttidMeasurement?.unit, DurationSentryMeasurementUnit.milliSecond); - expect( - ttidMeasurement?.value, - ttidSpan.endTimestamp! - .difference(ttidSpan.startTimestamp) - .inMilliseconds); - }); - }); - group('regular route', () { test( 'approximation tracking creates and finishes ttid span with correct measurements', From 461d2079e2166b5fc00d6b64a16cb4d68e13fb05 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 14:06:32 +0200 Subject: [PATCH 13/20] cleanup --- .../native_app_start_integration.dart | 20 +++---------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index adfbfd200e..a1112dd4a0 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -33,14 +33,14 @@ class NativeAppStartIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) async { if (!options.autoAppStart && _appStartEnd == null) { - await callThrowing(options, () async { + await _callThrowing(options, () async { await _appStartEndCompleter.future.timeout(const Duration(seconds: 10)); await _nativeAppStartHandler.call(hub, options, appStartEnd: _appStartEnd); }); } else { _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - await callThrowing(options, () async { + await _callThrowing(options, () async { await _nativeAppStartHandler.call(hub, options, appStartEnd: _appStartEnd); }); @@ -49,7 +49,7 @@ class NativeAppStartIntegration extends Integration { options.sdk.addIntegration('nativeAppStartIntegration'); } - Future callThrowing( + Future _callThrowing( SentryOptions options, Future Function() callback) async { try { await callback(); @@ -62,18 +62,4 @@ class NativeAppStartIntegration extends Integration { ); } } - - Future callHandler(Hub hub, SentryFlutterOptions options) async { - try { - await _nativeAppStartHandler.call(hub, options, - appStartEnd: _appStartEnd); - } catch (exception, stackTrace) { - options.logger( - SentryLevel.error, - 'Error while capturing native app start', - exception: exception, - stackTrace: stackTrace, - ); - } - } } From affdf8741ea9af207849e89bfcdbd58f4c342d7f Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 14:31:25 +0200 Subject: [PATCH 14/20] Fix blockng ui issue --- .../native_app_start_integration.dart | 48 ++++++++----------- .../native_app_start_integration_test.dart | 8 +++- 2 files changed, 26 insertions(+), 30 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index a1112dd4a0..a82c1cff83 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -32,34 +32,26 @@ class NativeAppStartIntegration extends Integration { @override void call(Hub hub, SentryFlutterOptions options) async { - if (!options.autoAppStart && _appStartEnd == null) { - await _callThrowing(options, () async { - await _appStartEndCompleter.future.timeout(const Duration(seconds: 10)); - await _nativeAppStartHandler.call(hub, options, - appStartEnd: _appStartEnd); - }); - } else { - _frameCallbackHandler.addPostFrameCallback((timeStamp) async { - await _callThrowing(options, () async { - await _nativeAppStartHandler.call(hub, options, - appStartEnd: _appStartEnd); - }); - }); - } + _frameCallbackHandler.addPostFrameCallback((timeStamp) async { + try { + if (!options.autoAppStart && _appStartEnd == null) { + await _appStartEndCompleter.future + .timeout(const Duration(seconds: 10)); + } + await _nativeAppStartHandler.call( + hub, + options, + appStartEnd: _appStartEnd, + ); + } catch (exception, stackTrace) { + options.logger( + SentryLevel.error, + 'Error while capturing native app start', + exception: exception, + stackTrace: stackTrace, + ); + } + }); options.sdk.addIntegration('nativeAppStartIntegration'); } - - Future _callThrowing( - SentryOptions options, Future Function() callback) async { - try { - await callback(); - } catch (exception, stackTrace) { - options.logger( - SentryLevel.error, - 'Error while capturing native app start', - exception: exception, - stackTrace: stackTrace, - ); - } - } } diff --git a/flutter/test/integrations/native_app_start_integration_test.dart b/flutter/test/integrations/native_app_start_integration_test.dart index 00cee0e5bc..02dbee7116 100644 --- a/flutter/test/integrations/native_app_start_integration_test.dart +++ b/flutter/test/integrations/native_app_start_integration_test.dart @@ -56,6 +56,8 @@ void main() { fixture.options.autoAppStart = false; fixture.callIntegration(); + final postFrameCallback = fixture.frameCallbackHandler.postFrameCallback!; + postFrameCallback(Duration(seconds: 0)); expect(fixture.nativeAppStartHandler.calls, 0); @@ -64,7 +66,7 @@ void main() { await Future.delayed(Duration(milliseconds: 10)); - expect(fixture.frameCallbackHandler.postFrameCallback, isNull); + expect(fixture.frameCallbackHandler.postFrameCallback, isNotNull); expect(fixture.nativeAppStartHandler.calls, 1); expect(fixture.nativeAppStartHandler.appStartEnd, appStartEnd); }); @@ -75,12 +77,14 @@ void main() { fixture.options.autoAppStart = false; fixture.callIntegration(); + final postFrameCallback = fixture.frameCallbackHandler.postFrameCallback!; + postFrameCallback(Duration(seconds: 0)); expect(fixture.nativeAppStartHandler.calls, 0); await Future.delayed(Duration(seconds: 11)); - expect(fixture.frameCallbackHandler.postFrameCallback, isNull); + expect(fixture.frameCallbackHandler.postFrameCallback, isNotNull); expect(fixture.nativeAppStartHandler.calls, 0); expect(fixture.nativeAppStartHandler.appStartEnd, null); }); From 43c49605232402c838b97ca593dce100c8fbf1b1 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 14:57:03 +0200 Subject: [PATCH 15/20] =?UTF-8?q?don=E2=80=99t=20create=20duplicate=20root?= =?UTF-8?q?=20transaction=20in=20sentry=20navigator=20observer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../native_app_start_handler.dart | 2 +- .../navigation/sentry_navigator_observer.dart | 8 +------ .../test/sentry_navigator_observer_test.dart | 21 +++++++------------ 3 files changed, 9 insertions(+), 22 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_handler.dart b/flutter/lib/src/integrations/native_app_start_handler.dart index b4ca3a9c90..a503d351d4 100644 --- a/flutter/lib/src/integrations/native_app_start_handler.dart +++ b/flutter/lib/src/integrations/native_app_start_handler.dart @@ -36,7 +36,7 @@ class NativeAppStartHandler { // Create Transaction & Span - const screenName = SentryNavigatorObserver.rootScreenName; + const screenName = 'root /'; final transaction = _hub.startTransaction( screenName, SentrySpanOperations.uiLoad, diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 7ec2cf7d1a..d65bdfb51f 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -243,13 +243,10 @@ class SentryNavigatorObserver extends RouteObserver> { String? name = _getRouteName(route); final arguments = route?.settings.arguments; - if (name == null) { + if (name == null || (name == '/')) { return; } - if (name == '/') { - name = rootScreenName; - } final transactionContext = SentryTransactionContext( name, SentrySpanOperations.uiLoad, @@ -381,9 +378,6 @@ class SentryNavigatorObserver extends RouteObserver> { _timeToDisplayTracker?.clear(); } - @internal - static const String rootScreenName = 'root /'; - bool _isRouteIgnored(Route route) { return _ignoreRoutes.isNotEmpty && _ignoreRoutes.contains(_getRouteName(route)); diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 41a7b828f0..f14f4aa114 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -486,39 +486,32 @@ void main() { verify(span.setData('route_settings_arguments', arguments)); }); - test('flutter root name is replaced', () async { + test('root route does not start transaction', () async { final rootRoute = route(RouteSettings(name: '/')); final hub = _MockHub(); - final span = getMockSentryTracer(name: '/'); + 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); sut.didPush(rootRoute, null); - await Future.delayed(const Duration(milliseconds: 100)); - final context = verify(hub.startTransactionWithContext( - captureAny, - waitForChildren: true, + verifyNever(hub.startTransactionWithContext( + any, startTimestamp: anyNamed('startTimestamp'), + waitForChildren: true, autoFinishAfter: anyNamed('autoFinishAfter'), trimEnd: true, onFinish: anyNamed('onFinish'), - )).captured.single as SentryTransactionContext; - - expect(context.name, 'root /'); + )); hub.configureScope((scope) { - expect(scope.span, span); + expect(scope.span, null); }); }); From 8f58f3cc881c045dc41b0a5d240674d4b66ba8a0 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 14:59:37 +0200 Subject: [PATCH 16/20] fix analyzer issue --- flutter/example/lib/user_feedback_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/example/lib/user_feedback_dialog.dart b/flutter/example/lib/user_feedback_dialog.dart index 495c9c7b7a..40d877b6f3 100644 --- a/flutter/example/lib/user_feedback_dialog.dart +++ b/flutter/example/lib/user_feedback_dialog.dart @@ -143,7 +143,7 @@ class _SentryLogo extends StatelessWidget { var color = Colors.white; final brightenss = Theme.of(context).brightness; if (brightenss == Brightness.light) { - color = const Color(0xff362d59).withOpacity(1.0); + color = const Color(0xff362d59).withValues(alpha: 1.0); } return FittedBox( From 3f1ea182e64a4f7ef8beb71dae9debbaee037692 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 15:02:37 +0200 Subject: [PATCH 17/20] remove useless method call --- flutter/example/lib/user_feedback_dialog.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/example/lib/user_feedback_dialog.dart b/flutter/example/lib/user_feedback_dialog.dart index 40d877b6f3..159f3c69d9 100644 --- a/flutter/example/lib/user_feedback_dialog.dart +++ b/flutter/example/lib/user_feedback_dialog.dart @@ -143,7 +143,7 @@ class _SentryLogo extends StatelessWidget { var color = Colors.white; final brightenss = Theme.of(context).brightness; if (brightenss == Brightness.light) { - color = const Color(0xff362d59).withValues(alpha: 1.0); + color = const Color(0xff362d59); } return FittedBox( From 9e7c6144a0bce67434994eb5f81d4234ecf9a4bc Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 15:26:32 +0200 Subject: [PATCH 18/20] revert deprecation of SentryFlutter.setAppStartEnd --- flutter/lib/src/integrations/native_app_start_integration.dart | 1 - flutter/lib/src/sentry_flutter.dart | 1 - flutter/lib/src/sentry_flutter_options.dart | 1 - 3 files changed, 3 deletions(-) diff --git a/flutter/lib/src/integrations/native_app_start_integration.dart b/flutter/lib/src/integrations/native_app_start_integration.dart index a82c1cff83..87097112fd 100644 --- a/flutter/lib/src/integrations/native_app_start_integration.dart +++ b/flutter/lib/src/integrations/native_app_start_integration.dart @@ -17,7 +17,6 @@ class NativeAppStartIntegration extends Integration { DateTime? _appStartEnd; /// This timestamp marks the end of app startup. Either set by calling - // ignore: deprecated_member_use_from_same_package /// [SentryFlutter.setAppStartEnd]. The [SentryFlutterOptions.autoAppStart] /// option needs to be false. @internal diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index 955979d2bc..f4d1f1d8d8 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -230,7 +230,6 @@ mixin SentryFlutter { /// Manually set when your app finished startup. Make sure to set /// [SentryFlutterOptions.autoAppStart] to false on init. The timeout duration /// for this to work is 10 seconds. - @Deprecated('Will be removed in a future release.') static void setAppStartEnd(DateTime appStartEnd) { // ignore: invalid_use_of_internal_member final integrations = Sentry.currentHub.options.integrations diff --git a/flutter/lib/src/sentry_flutter_options.dart b/flutter/lib/src/sentry_flutter_options.dart index d8d6582130..18b9d8c919 100644 --- a/flutter/lib/src/sentry_flutter_options.dart +++ b/flutter/lib/src/sentry_flutter_options.dart @@ -176,7 +176,6 @@ class SentryFlutterOptions extends SentryOptions { /// Automatically track app start measurement and send it with the /// first transaction. Set to false when configuring option to disable or if /// you want to set the end time of app startup manually using - // ignore: deprecated_member_use_from_same_package /// [SentryFlutter.setAppStartEnd]. bool autoAppStart = true; From d04a57cd04a28ec002bcc8fc8925b840cc0c9628 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Wed, 11 Sep 2024 15:32:27 +0200 Subject: [PATCH 19/20] amend changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a544c5e05b..cca4713741 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ ### Enhancements - Improve app start integration ([#2266](https://github.com/getsentry/sentry-dart/pull/2266)) + - Fixes ([#2103](https://github.com/getsentry/sentry-dart/issues/2103)) + - Fixes ([#2233](https://github.com/getsentry/sentry-dart/issues/2233)) ## 8.9.0 From 02ce10e1cd07c983a3d866e95e2bac50e0689629 Mon Sep 17 00:00:00 2001 From: Denis Andrasec Date: Tue, 17 Sep 2024 11:38:44 +0200 Subject: [PATCH 20/20] use final in loop --- flutter/lib/src/sentry_flutter.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index b370350d16..e190115149 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -222,7 +222,7 @@ mixin SentryFlutter { // ignore: invalid_use_of_internal_member final integrations = Sentry.currentHub.options.integrations .whereType(); - for (var integration in integrations) { + for (final integration in integrations) { integration.appStartEnd = appStartEnd; } }