From 41495521cb0cd8bd42d1cde925662c9b082ff6e8 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 15:31:42 +0200 Subject: [PATCH 01/10] update --- flutter/lib/src/navigation/sentry_navigator_observer.dart | 7 ++++++- .../lib/src/navigation/time_to_full_display_tracker.dart | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 2279d6e434..79ce8a28fe 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -8,6 +8,8 @@ import 'package:meta/meta.dart'; import '../native/native_frames.dart'; import '../native/sentry_native_binding.dart'; import 'time_to_display_tracker.dart'; +import 'time_to_full_display_tracker.dart'; +import 'time_to_initial_display_tracker.dart'; import '../../sentry_flutter.dart'; import '../event_processor/flutter_enricher_event_processor.dart'; @@ -313,10 +315,13 @@ class SentryNavigatorObserver extends RouteObserver> { SentrySpanOperations.uiTimeToInitialDisplay; final isTTFDSpan = child.context.operation == SentrySpanOperations.uiTimeToFullDisplay; + if (isTTFDSpan) { + endTimestamp = ttidEndTimestampProvider() ?? endTimestamp; + } if (!child.finished && (isTTIDSpan || isTTFDSpan)) { await child.finish( endTimestamp: endTimestamp, - status: SpanStatus.deadlineExceeded(), + status: SpanStatus.cancelled(), ); } } diff --git a/flutter/lib/src/navigation/time_to_full_display_tracker.dart b/flutter/lib/src/navigation/time_to_full_display_tracker.dart index 6c9130eebf..654d8ac864 100644 --- a/flutter/lib/src/navigation/time_to_full_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_full_display_tracker.dart @@ -35,7 +35,7 @@ class TimeToFullDisplayTracker { final options = Sentry.currentHub.options; // End timestamp provider is only needed when the TTFD timeout is triggered - EndTimestampProvider _endTimestampProvider = ttidEndTimestampProvider(); + EndTimestampProvider _endTimestampProvider = ttidEndTimestampProvider; Completer _completedTTFDTracking = Completer(); Future track({ @@ -115,5 +115,5 @@ class TimeToFullDisplayTracker { typedef EndTimestampProvider = DateTime? Function(); @internal -EndTimestampProvider ttidEndTimestampProvider() => +EndTimestampProvider ttidEndTimestampProvider = () => TimeToInitialDisplayTracker().endTimestamp; From 9ba347022d3a7529d64a2e2a1cdfa871c2567fe2 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 15:56:16 +0200 Subject: [PATCH 02/10] update --- .../lib/src/navigation/sentry_navigator_observer.dart | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 79ce8a28fe..9f702ba020 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -311,16 +311,18 @@ class SentryNavigatorObserver extends RouteObserver> { // Cancel unfinished TTID/TTFD spans, e.g this might happen if the user navigates // away from the current route before TTFD or TTID is finished. for (final child in (transaction as SentryTracer).children) { + if (child.finished) continue; + final isTTIDSpan = child.context.operation == SentrySpanOperations.uiTimeToInitialDisplay; final isTTFDSpan = child.context.operation == SentrySpanOperations.uiTimeToFullDisplay; - if (isTTFDSpan) { - endTimestamp = ttidEndTimestampProvider() ?? endTimestamp; - } if (!child.finished && (isTTIDSpan || isTTFDSpan)) { + final finishTimestamp = isTTFDSpan + ? (ttidEndTimestampProvider() ?? endTimestamp) + : endTimestamp; await child.finish( - endTimestamp: endTimestamp, + endTimestamp: finishTimestamp, status: SpanStatus.cancelled(), ); } From cb41d717737ad0355bfcf7c8f18c0c68c95d96a8 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 16:14:18 +0200 Subject: [PATCH 03/10] update --- .../test/sentry_navigator_observer_test.dart | 93 ++++++++++++++++--- 1 file changed, 82 insertions(+), 11 deletions(-) diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index c43ccfece1..67af4c6349 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -380,8 +380,83 @@ void main() { .called(1); }); - test( - 'unfinished children will be finished with deadline_exceeded on didPush', + test('cancelled TTID and TTFD spans do not add measurements', () async { + final initialRoute = route(RouteSettings(name: 'Initial Route')); + final newRoute = route(RouteSettings(name: 'New Route')); + + final hub = _MockHub(); + final transaction = getMockSentryTracer(finished: false) as SentryTracer; + + final mockChildTTID = MockSentrySpan(); + final mockChildTTFD = MockSentrySpan(); + + when(transaction.children).thenReturn([ + mockChildTTID, + mockChildTTFD, + ]); + + when(transaction.measurements).thenReturn({}); + + when(mockChildTTID.finished).thenReturn(false); + when(mockChildTTID.context).thenReturn(SentrySpanContext( + operation: SentrySpanOperations.uiTimeToInitialDisplay)); + when(mockChildTTID.status).thenReturn(SpanStatus.cancelled()); + + when(mockChildTTFD.finished).thenReturn(false); + when(mockChildTTFD.context).thenReturn(SentrySpanContext( + operation: SentrySpanOperations.uiTimeToFullDisplay)); + when(mockChildTTFD.status).thenReturn(SpanStatus.cancelled()); + + when(transaction.context) + .thenReturn(SentrySpanContext(operation: 'navigation')); + when(transaction.status).thenReturn(null); + when(transaction.finished).thenReturn(false); + + when(transaction.startChild( + 'ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'), + )).thenReturn(MockSentrySpan()); + + when(hub.getSpan()).thenReturn(transaction); + when(hub.startTransactionWithContext( + any, + startTimestamp: anyNamed('startTimestamp'), + waitForChildren: true, + autoFinishAfter: anyNamed('autoFinishAfter'), + trimEnd: true, + onFinish: anyNamed('onFinish'), + )).thenReturn(transaction); + + final sut = + fixture.getSut(hub: hub, autoFinishAfter: Duration(seconds: 5)); + + // Simulate pushing the initial route + sut.didPush(initialRoute, null); + + // Simulate navigating to a new route before TTID and TTFD spans finish + sut.didPush(newRoute, initialRoute); + + // Allow async operations to complete + await Future.delayed(const Duration(milliseconds: 100)); + + // Verify that the TTID and TTFD spans are finished with a cancelled status + verify(mockChildTTID.finish( + endTimestamp: anyNamed('endTimestamp'), + status: SpanStatus.cancelled())) + .called(1); + verify(mockChildTTFD.finish( + endTimestamp: anyNamed('endTimestamp'), + status: SpanStatus.cancelled())) + .called(1); + + // Verify that the measurements are not added to the transaction + final measurements = transaction.measurements; + expect(measurements.containsKey('time_to_initial_display'), isFalse); + expect(measurements.containsKey('time_to_full_display'), isFalse); + }); + + test('unfinished children will be finished with cancelled on didPush', () async { final currentRoute = route(RouteSettings(name: 'Current Route')); @@ -409,26 +484,22 @@ void main() { final sut = fixture.getSut(hub: hub); - // Push to new screen, e.g app start / root screen sut.didPush(currentRoute, null); - - // Push to screen e.g root to user screen sut.didPush(currentRoute, null); await Future.delayed(const Duration(milliseconds: 100)); verify(mockChildA.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.deadlineExceeded())) + status: SpanStatus.cancelled())) .called(1); verify(mockChildB.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.deadlineExceeded())) + status: SpanStatus.cancelled())) .called(1); }); - test( - 'unfinished children will be finished with deadline_exceeded on didPop', + test('unfinished children will be finished with cancelled on didPop', () async { final currentRoute = route(RouteSettings(name: 'Current Route')); @@ -466,11 +537,11 @@ void main() { verify(mockChildA.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.deadlineExceeded())) + status: SpanStatus.cancelled())) .called(1); verify(mockChildB.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.deadlineExceeded())) + status: SpanStatus.cancelled())) .called(1); }); From 697a23ac0dea59d20a9f66070efc03508d58f387 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 16:16:46 +0200 Subject: [PATCH 04/10] update --- flutter/lib/src/navigation/sentry_navigator_observer.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 9f702ba020..709b1e9832 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -9,7 +9,6 @@ import '../native/native_frames.dart'; import '../native/sentry_native_binding.dart'; import 'time_to_display_tracker.dart'; import 'time_to_full_display_tracker.dart'; -import 'time_to_initial_display_tracker.dart'; import '../../sentry_flutter.dart'; import '../event_processor/flutter_enricher_event_processor.dart'; @@ -317,7 +316,7 @@ class SentryNavigatorObserver extends RouteObserver> { SentrySpanOperations.uiTimeToInitialDisplay; final isTTFDSpan = child.context.operation == SentrySpanOperations.uiTimeToFullDisplay; - if (!child.finished && (isTTIDSpan || isTTFDSpan)) { + if (isTTIDSpan || isTTFDSpan) { final finishTimestamp = isTTFDSpan ? (ttidEndTimestampProvider() ?? endTimestamp) : endTimestamp; From 17a4bce51b397e2fe0d3688ca4f8b59b136dccc6 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 16:21:57 +0200 Subject: [PATCH 05/10] update --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7aabdc80c6..d676d8b26b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,6 +64,8 @@ Navigator.push( - iOS replay integration when only `onErrorSampleRate` is specified ([#2306](https://github.com/getsentry/sentry-dart/pull/2306)) - Fix TTID timing issue ([#2326](https://github.com/getsentry/sentry-dart/pull/2326)) - Start missing TTFD for root screen transaction ([#2332](https://github.com/getsentry/sentry-dart/pull/2332)) +- Unfinished TTID or TTFD spans should be set to status `cancelled` ([#2347](https://github.com/getsentry/sentry-dart/pull/2347)) + - This might happen if a user navigates to another screen while either span is unfinished - Accessing invalid json fields from `fetchNativeAppStart` should return null ([#2340](https://github.com/getsentry/sentry-dart/pull/2340)) - Error when calling `SentryFlutter.reportFullyDisplayed()` twice ([#2339](https://github.com/getsentry/sentry-dart/pull/2339)) From ad09517cfe0d28f92bc422ea2939f176eaf84c6a Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 16:22:32 +0200 Subject: [PATCH 06/10] fix compilation --- flutter/test/navigation/time_to_display_tracker_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/test/navigation/time_to_display_tracker_test.dart b/flutter/test/navigation/time_to_display_tracker_test.dart index b77f12d082..d4cd87cb6c 100644 --- a/flutter/test/navigation/time_to_display_tracker_test.dart +++ b/flutter/test/navigation/time_to_display_tracker_test.dart @@ -198,7 +198,7 @@ class Fixture { final options = defaultTestOptions() ..dsn = fakeDsn ..tracesSampleRate = 1.0; - late final endTimeProvider = ttidEndTimestampProvider(); + late final endTimeProvider = ttidEndTimestampProvider; late final hub = Hub(options); TimeToInitialDisplayTracker? ttidTracker; From f3c3971cd2ee424a475e7325b4a855b2e9c75364 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Fri, 11 Oct 2024 18:08:59 +0200 Subject: [PATCH 07/10] update --- CHANGELOG.md | 3 +-- .../navigation/sentry_navigator_observer.dart | 2 +- .../test/sentry_navigator_observer_test.dart | 19 +++++++++++-------- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d676d8b26b..89f135dccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -64,8 +64,7 @@ Navigator.push( - iOS replay integration when only `onErrorSampleRate` is specified ([#2306](https://github.com/getsentry/sentry-dart/pull/2306)) - Fix TTID timing issue ([#2326](https://github.com/getsentry/sentry-dart/pull/2326)) - Start missing TTFD for root screen transaction ([#2332](https://github.com/getsentry/sentry-dart/pull/2332)) -- Unfinished TTID or TTFD spans should be set to status `cancelled` ([#2347](https://github.com/getsentry/sentry-dart/pull/2347)) - - This might happen if a user navigates to another screen while either span is unfinished +- Match TTFD to TTID end timespan if TTFD is unfinished when user navigates to another screen ([#2347](https://github.com/getsentry/sentry-dart/pull/2347)) - Accessing invalid json fields from `fetchNativeAppStart` should return null ([#2340](https://github.com/getsentry/sentry-dart/pull/2340)) - Error when calling `SentryFlutter.reportFullyDisplayed()` twice ([#2339](https://github.com/getsentry/sentry-dart/pull/2339)) diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 709b1e9832..6717a425d0 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -322,7 +322,7 @@ class SentryNavigatorObserver extends RouteObserver> { : endTimestamp; await child.finish( endTimestamp: finishTimestamp, - status: SpanStatus.cancelled(), + status: SpanStatus.deadlineExceeded(), ); } } diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 67af4c6349..b3a48464f2 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -380,6 +380,7 @@ void main() { .called(1); }); + // e.g when a user navigates to another screen before ttfd or ttid is finished test('cancelled TTID and TTFD spans do not add measurements', () async { final initialRoute = route(RouteSettings(name: 'Initial Route')); final newRoute = route(RouteSettings(name: 'New Route')); @@ -443,11 +444,11 @@ void main() { // Verify that the TTID and TTFD spans are finished with a cancelled status verify(mockChildTTID.finish( endTimestamp: anyNamed('endTimestamp'), - status: SpanStatus.cancelled())) + status: SpanStatus.deadlineExceeded())) .called(1); verify(mockChildTTFD.finish( endTimestamp: anyNamed('endTimestamp'), - status: SpanStatus.cancelled())) + status: SpanStatus.deadlineExceeded())) .called(1); // Verify that the measurements are not added to the transaction @@ -456,7 +457,8 @@ void main() { expect(measurements.containsKey('time_to_full_display'), isFalse); }); - test('unfinished children will be finished with cancelled on didPush', + test( + 'unfinished children will be finished with deadline_exceeded on didPush', () async { final currentRoute = route(RouteSettings(name: 'Current Route')); @@ -491,15 +493,16 @@ void main() { verify(mockChildA.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.cancelled())) + status: SpanStatus.deadlineExceeded())) .called(1); verify(mockChildB.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.cancelled())) + status: SpanStatus.deadlineExceeded())) .called(1); }); - test('unfinished children will be finished with cancelled on didPop', + test( + 'unfinished children will be finished with deadline_exceeded on didPop', () async { final currentRoute = route(RouteSettings(name: 'Current Route')); @@ -537,11 +540,11 @@ void main() { verify(mockChildA.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.cancelled())) + status: SpanStatus.deadlineExceeded())) .called(1); verify(mockChildB.finish( endTimestamp: captureAnyNamed('endTimestamp'), - status: SpanStatus.cancelled())) + status: SpanStatus.deadlineExceeded())) .called(1); }); From 099b8d1f8e353ca8f2f34b9682ab565c514aec2c Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 14 Oct 2024 15:24:02 +0200 Subject: [PATCH 08/10] add tesT --- .../test/sentry_navigator_observer_test.dart | 87 +++++++++++++++---- 1 file changed, 70 insertions(+), 17 deletions(-) diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index b3a48464f2..846a0fc1d3 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -457,45 +457,98 @@ void main() { expect(measurements.containsKey('time_to_full_display'), isFalse); }); + test('unfinished ttfd will match ttid duration if available', () async { + final currentRoute = route(RouteSettings(name: 'Current Route')); + + final hub = _MockHub(); + final transaction = getMockSentryTracer(finished: false) as SentryTracer; + final ttidSpan = MockSentrySpan(); + final ttfdSpan = MockSentrySpan(); + when(transaction.children).thenReturn([ + ttfdSpan, + ttidSpan, + ]); + when(ttidSpan.finished).thenReturn(false); + when(ttfdSpan.finished).thenReturn(false); + when(ttidSpan.context).thenReturn(SentrySpanContext( + operation: SentrySpanOperations.uiTimeToInitialDisplay)); + when(ttfdSpan.context).thenReturn(SentrySpanContext( + operation: SentrySpanOperations.uiTimeToFullDisplay)); + when(transaction.context).thenReturn(SentrySpanContext(operation: 'op')); + when(transaction.status).thenReturn(null); + when(transaction.startChild('ui.load.initial_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); + _whenAnyStart(hub, transaction); + + final sut = fixture.getSut(hub: hub); + + sut.didPush(currentRoute, null); + + final anotherRoute = route(RouteSettings(name: 'Another Route')); + sut.didPush(anotherRoute, null); + + await Future.delayed(const Duration(milliseconds: 100)); + + final ttidFinishVerification = verify(ttidSpan.finish( + endTimestamp: captureAnyNamed('endTimestamp'), + status: anyNamed('status'), + )); + final ttidEndTimestamp = + ttidFinishVerification.captured.single as DateTime; + + final ttfdFinishVerification = verify(ttfdSpan.finish( + endTimestamp: captureAnyNamed('endTimestamp'), + status: anyNamed('status'), + )); + final ttfdEndTimestamp = + ttfdFinishVerification.captured.single as DateTime; + + expect(ttfdEndTimestamp, equals(ttidEndTimestamp)); + }); + test( 'unfinished children will be finished with deadline_exceeded on didPush', () async { final currentRoute = route(RouteSettings(name: 'Current Route')); final hub = _MockHub(); - final span = getMockSentryTracer(finished: false) as SentryTracer; - final mockChildA = MockSentrySpan(); - final mockChildB = MockSentrySpan(); - when(span.children).thenReturn([ - mockChildB, - mockChildA, + final transaction = getMockSentryTracer(finished: false) as SentryTracer; + final ttidSpan = MockSentrySpan(); + final ttfdSpan = MockSentrySpan(); + when(transaction.children).thenReturn([ + ttfdSpan, + ttidSpan, ]); - when(mockChildA.finished).thenReturn(false); - when(mockChildB.finished).thenReturn(false); - when(mockChildA.context).thenReturn(SentrySpanContext( + when(ttidSpan.finished).thenReturn(false); + when(ttfdSpan.finished).thenReturn(false); + when(ttidSpan.context).thenReturn(SentrySpanContext( operation: SentrySpanOperations.uiTimeToInitialDisplay)); - when(mockChildB.context).thenReturn(SentrySpanContext( + when(ttfdSpan.context).thenReturn(SentrySpanContext( operation: SentrySpanOperations.uiTimeToFullDisplay)); - when(span.context).thenReturn(SentrySpanContext(operation: 'op')); - when(span.status).thenReturn(null); - when(span.startChild('ui.load.initial_display', + when(transaction.context).thenReturn(SentrySpanContext(operation: 'op')); + when(transaction.status).thenReturn(null); + when(transaction.startChild('ui.load.initial_display', description: anyNamed('description'), startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); - _whenAnyStart(hub, span); + _whenAnyStart(hub, transaction); final sut = fixture.getSut(hub: hub); sut.didPush(currentRoute, null); - sut.didPush(currentRoute, null); + + final anotherRoute = route(RouteSettings(name: 'Another Route')); + sut.didPush(anotherRoute, null); await Future.delayed(const Duration(milliseconds: 100)); - verify(mockChildA.finish( + verify(ttidSpan.finish( endTimestamp: captureAnyNamed('endTimestamp'), status: SpanStatus.deadlineExceeded())) .called(1); - verify(mockChildB.finish( + verify(ttfdSpan.finish( endTimestamp: captureAnyNamed('endTimestamp'), status: SpanStatus.deadlineExceeded())) .called(1); From 78301f72d512f5ff204faf495fc677aa1a5e4d99 Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 14 Oct 2024 15:32:11 +0200 Subject: [PATCH 09/10] fix timezone --- flutter/test/sentry_navigator_observer_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index 846a0fc1d3..fe2f029c4f 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -505,7 +505,7 @@ void main() { final ttfdEndTimestamp = ttfdFinishVerification.captured.single as DateTime; - expect(ttfdEndTimestamp, equals(ttidEndTimestamp)); + expect(ttfdEndTimestamp.toUtc(), equals(ttidEndTimestamp.toUtc())); }); test( From 8c7f60b4c68876415bdb46ed568ff1e058da4e9d Mon Sep 17 00:00:00 2001 From: GIancarlo Buenaflor Date: Mon, 14 Oct 2024 16:22:58 +0200 Subject: [PATCH 10/10] fix tests --- .../time_to_initial_display_tracker.dart | 6 ++++ .../test/sentry_navigator_observer_test.dart | 29 ++++++++++++++++++- 2 files changed, 34 insertions(+), 1 deletion(-) 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 d2bad18690..5213d4587c 100644 --- a/flutter/lib/src/navigation/time_to_initial_display_tracker.dart +++ b/flutter/lib/src/navigation/time_to_initial_display_tracker.dart @@ -129,4 +129,10 @@ class TimeToInitialDisplayTracker { // We can't clear the ttid end time stamp here, because it might be needed // in the [TimeToFullDisplayTracker] class } + + @visibleForTesting + void clearForTest() { + clear(); + _endTimestamp = null; + } } diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index fe2f029c4f..cc7fc106e5 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -10,6 +10,7 @@ import 'package:sentry_flutter/sentry_flutter.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'; +import 'package:sentry_flutter/src/navigation/time_to_full_display_tracker.dart'; import 'package:sentry_flutter/src/navigation/time_to_initial_display_tracker.dart'; import 'fake_frame_callback_handler.dart'; @@ -128,6 +129,11 @@ void main() { }); group('$SentryNavigatorObserver', () { + tearDown(() { + fixture.timeToInitialDisplayTracker.clearForTest(); + fixture.timeToFullDisplayTracker.clear(); + }); + test('didPush starts transaction', () async { const name = 'Current Route'; final currentRoute = route(RouteSettings(name: name)); @@ -461,6 +467,9 @@ void main() { final currentRoute = route(RouteSettings(name: 'Current Route')); final hub = _MockHub(); + final options = hub.options as SentryFlutterOptions; + options.enableTimeToFullDisplayTracing = true; + final transaction = getMockSentryTracer(finished: false) as SentryTracer; final ttidSpan = MockSentrySpan(); final ttfdSpan = MockSentrySpan(); @@ -480,6 +489,10 @@ void main() { description: anyNamed('description'), startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); + when(transaction.startChild('ui.load.full_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, transaction); final sut = fixture.getSut(hub: hub); @@ -514,6 +527,9 @@ void main() { final currentRoute = route(RouteSettings(name: 'Current Route')); final hub = _MockHub(); + final options = hub.options as SentryFlutterOptions; + options.enableTimeToFullDisplayTracing = true; + final transaction = getMockSentryTracer(finished: false) as SentryTracer; final ttidSpan = MockSentrySpan(); final ttfdSpan = MockSentrySpan(); @@ -533,6 +549,10 @@ void main() { description: anyNamed('description'), startTimestamp: anyNamed('startTimestamp'))) .thenReturn(NoOpSentrySpan()); + when(transaction.startChild('ui.load.full_display', + description: anyNamed('description'), + startTimestamp: anyNamed('startTimestamp'))) + .thenReturn(NoOpSentrySpan()); _whenAnyStart(hub, transaction); final sut = fixture.getSut(hub: hub); @@ -1165,6 +1185,9 @@ void main() { } class Fixture { + late TimeToInitialDisplayTracker timeToInitialDisplayTracker; + late TimeToFullDisplayTracker timeToFullDisplayTracker; + SentryNavigatorObserver getSut({ required Hub hub, bool enableAutoTransactions = true, @@ -1175,13 +1198,17 @@ class Fixture { List? ignoreRoutes, }) { final frameCallbackHandler = FakeFrameCallbackHandler(); - final timeToInitialDisplayTracker = TimeToInitialDisplayTracker( + timeToInitialDisplayTracker = TimeToInitialDisplayTracker( frameCallbackHandler: frameCallbackHandler, ); + timeToFullDisplayTracker = TimeToFullDisplayTracker( + endTimestampProvider: () => timeToInitialDisplayTracker.endTimestamp, + ); final options = hub.options; if (options is SentryFlutterOptions) { options.timeToDisplayTracker = TimeToDisplayTracker( ttidTracker: timeToInitialDisplayTracker, + ttfdTracker: timeToFullDisplayTracker, options: hub.options as SentryFlutterOptions, ); }