Skip to content

feat: ttid #1910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 65 commits into from
Mar 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
0b21267
Change app start integration in a way that works with ttid as well
buenaflor Mar 1, 2024
6f6e71c
Formatting
buenaflor Mar 1, 2024
6ed7700
Update
buenaflor Mar 1, 2024
2f8a47d
add visibleForTesting
buenaflor Mar 1, 2024
50f298b
Update
buenaflor Mar 1, 2024
3477e6f
update
buenaflor Mar 4, 2024
dc80015
Merge branch 'main' into feat/ttid-appstart
buenaflor Mar 4, 2024
ed3d4ae
Add app start info test
buenaflor Mar 4, 2024
81346de
Remove set app start info null
buenaflor Mar 4, 2024
9d09211
Merge branch 'main' into feat/ttid-appstart
buenaflor Mar 4, 2024
cf5af40
Review improvements
buenaflor Mar 4, 2024
c653eb1
Merge branch 'main' into feat/ttid-appstart
buenaflor Mar 4, 2024
487e55f
Add TTID
buenaflor Mar 4, 2024
845e6a8
Improvements
buenaflor Mar 4, 2024
96c8766
Improvements
buenaflor Mar 4, 2024
1d9e71f
Fix integration test
buenaflor Mar 4, 2024
e3c227f
Merge branch 'main' into feat/ttid
buenaflor Mar 4, 2024
cea12c5
Update
buenaflor Mar 4, 2024
9af3455
Clear after tracking
buenaflor Mar 4, 2024
71fd7ec
Update CHANGELOG
buenaflor Mar 4, 2024
7a977fb
Format
buenaflor Mar 4, 2024
d7a6a83
Update
buenaflor Mar 4, 2024
cde560f
Update
buenaflor Mar 4, 2024
f66206d
remove import
buenaflor Mar 4, 2024
87d2755
Merge branch 'main' into feat/ttid
buenaflor Mar 4, 2024
4dedf37
Update sentry tracer
buenaflor Mar 4, 2024
5dd824e
Add (not all) improvements for pr review
buenaflor Mar 5, 2024
bf2ad2f
combine transaction handler
buenaflor Mar 5, 2024
4d33aab
Refactor trackAppStart and trackRegularRoute to use private method
buenaflor Mar 5, 2024
248cb8f
Fix dart analyzer
buenaflor Mar 5, 2024
35ec31c
Remove clear
buenaflor Mar 5, 2024
e1fde58
Clear in tearDown
buenaflor Mar 5, 2024
4efd9f9
Apply suggestions from code review
buenaflor Mar 5, 2024
579a3c5
Apply PR suggestions
buenaflor Mar 5, 2024
f2ba992
fix analyze
buenaflor Mar 5, 2024
7c64a3f
update
buenaflor Mar 5, 2024
d90a7ed
update
buenaflor Mar 5, 2024
b2767aa
Fix tests
buenaflor Mar 5, 2024
4c6301a
Fix analyze
buenaflor Mar 5, 2024
eaa6d8d
revert sample
buenaflor Mar 5, 2024
6ca06dd
Update
buenaflor Mar 5, 2024
c73de7b
Update
buenaflor Mar 5, 2024
238995e
Fix test
buenaflor Mar 6, 2024
133e166
Move clear to the beginning of function
buenaflor Mar 6, 2024
cc4398d
Fix start time
buenaflor Mar 6, 2024
c0f41d8
Fix analyze
buenaflor Mar 6, 2024
2dafed4
Merge branch 'main' into feat/ttid
buenaflor Mar 6, 2024
7434ca1
remove comment
buenaflor Mar 6, 2024
e0298fe
Formatting
buenaflor Mar 6, 2024
7e8478c
fix test
buenaflor Mar 6, 2024
67c6e8f
add ttid duration assertion and determineEndTime timeout
buenaflor Mar 6, 2024
d32bdea
Merge branch 'main' into feat/ttid
buenaflor Mar 6, 2024
320f92b
Rename finish transaction and do an early exit with enableAutoTransac…
buenaflor Mar 6, 2024
a823366
Rename function
buenaflor Mar 6, 2024
1cd1025
Merge branch 'main' into feat/ttid
buenaflor Mar 6, 2024
dbc4320
Remove static and getter for in navigator observer
buenaflor Mar 7, 2024
9c6e2b1
Expose SentryDisplayWidget as public api and add it to example app
buenaflor Mar 7, 2024
1aa2343
Fix dart analyze
buenaflor Mar 7, 2024
b15eccf
Fix dart doc
buenaflor Mar 7, 2024
aeaebfa
Improve tests
buenaflor Mar 7, 2024
5552732
Reduce fake frame finishing time and improve tests
buenaflor Mar 7, 2024
524935c
Improve test names
buenaflor Mar 7, 2024
6e86523
Fix tests
buenaflor Mar 7, 2024
5fa126e
Apply formatting
buenaflor Mar 7, 2024
d209276
Add extra assertion in tests
buenaflor Mar 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,16 @@

### Features

- Add TTID (time to initial display), which allows you to measure the time it takes to render the first frame of your screen ([#1910](https://github.com/getsentry/sentry-dart/pull/1910))
- Requires using the [routing instrumentation](https://docs.sentry.io/platforms/flutter/integrations/routing-instrumentation/).
- Introduces two modes:
- `automatic` mode is enabled by default for all screens and will yield only an approximation result.
- `manual` mode requires manual instrumentation and will yield a more accurate result.
- To use `manual` mode, you need to wrap your desired widget: `SentryDisplayWidget(child: MyScreen())`.
- You can mix and match both modes in your app.
- Other significant fixes
- `didPop` doesn't trigger a new transaction
- Change transaction operation name to `ui.load` instead of `navigation`
- Use `recordHttpBreadcrumbs` to set iOS `enableNetworkBreadcrumbs` ([#1884](https://github.com/getsentry/sentry-dart/pull/1884))
- Add `maxQueueSize` to limit the number of unawaited events sent to Sentry ([#1868](https://github.com/getsentry/sentry-dart/pull/1868))

Expand Down
2 changes: 2 additions & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ export 'src/utils/http_header_utils.dart';
// ignore: invalid_export_of_internal_element
export 'src/sentry_trace_origins.dart';
// ignore: invalid_export_of_internal_element
export 'src/sentry_span_operations.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils.dart';
// spotlight debugging
export 'src/spotlight.dart';
7 changes: 7 additions & 0 deletions dart/lib/src/sentry_measurement.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,13 @@ class SentryMeasurement {
value = duration.inMilliseconds,
unit = DurationSentryMeasurementUnit.milliSecond;

/// Duration of the time to initial display in milliseconds
SentryMeasurement.timeToInitialDisplay(Duration duration)
: assert(!duration.isNegative),
name = 'time_to_initial_display',
value = duration.inMilliseconds,
unit = DurationSentryMeasurementUnit.milliSecond;

final String name;
final num value;
final SentryMeasurementUnit? unit;
Expand Down
7 changes: 7 additions & 0 deletions dart/lib/src/sentry_span_operations.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import 'package:meta/meta.dart';

@internal
class SentrySpanOperations {
static const String uiLoad = 'ui.load';
static const String uiTimeToInitialDisplay = 'ui.load.initial_display';
}
2 changes: 2 additions & 0 deletions dart/lib/src/sentry_trace_origins.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ class SentryTraceOrigins {
static const autoDbDriftQueryExecutor = 'auto.db.drift.query.executor';
static const autoDbDriftTransactionExecutor =
'auto.db.drift.transaction.executor';
static const autoUiTimeToDisplay = 'auto.ui.time_to_display';
static const manualUiTimeToDisplay = 'manual.ui.time_to_display';
}
2 changes: 1 addition & 1 deletion flutter/example/android/app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ android {

defaultConfig {
applicationId "io.sentry.samples.flutter"
minSdkVersion 19
minSdkVersion flutter.minSdkVersion
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
Expand Down
4 changes: 3 additions & 1 deletion flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_isar/sentry_isar.dart';
import 'package:sentry_sqflite/sentry_sqflite.dart';
import 'package:sqflite/sqflite.dart';

// import 'package:sqflite_common_ffi/sqflite_ffi.dart';
// import 'package:sqflite_common_ffi_web/sqflite_ffi_web.dart';
import 'package:universal_platform/universal_platform.dart';
Expand Down Expand Up @@ -80,6 +81,7 @@ Future<void> setupSentry(
// going to log too much for your app, but can be useful when figuring out
// configuration issues, e.g. finding out why your events are not uploaded.
options.debug = true;
options.spotlight = Spotlight(enabled: true);

options.maxRequestBodySize = MaxRequestBodySize.always;
options.maxResponseBodySize = MaxResponseBodySize.always;
Expand Down Expand Up @@ -732,7 +734,7 @@ void navigateToAutoCloseScreen(BuildContext context) {
context,
MaterialPageRoute(
settings: const RouteSettings(name: 'AutoCloseScreen'),
builder: (context) => const AutoCloseScreen(),
builder: (context) => SentryDisplayWidget(child: const AutoCloseScreen()),
),
);
}
Expand Down
1 change: 1 addition & 0 deletions flutter/lib/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,3 +16,4 @@ export 'src/screenshot/sentry_screenshot_quality.dart';
export 'src/user_interaction/sentry_user_interaction_widget.dart';
export 'src/binding_wrapper.dart';
export 'src/sentry_widget.dart';
export 'src/navigation/sentry_display_widget.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,9 @@ class AppStartInfo {
final AppStartType type;
final DateTime start;
final DateTime end;
Duration get duration => end.difference(start);

SentryMeasurement toMeasurement() {
final duration = end.difference(start);
return type == AppStartType.cold
? SentryMeasurement.coldAppStart(duration)
: SentryMeasurement.warmAppStart(duration);
Expand Down
60 changes: 60 additions & 0 deletions flutter/lib/src/navigation/sentry_display_widget.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import 'package:flutter/cupertino.dart';
import 'time_to_initial_display_tracker.dart';

import '../frame_callback_handler.dart';

/// A widget that reports the Time To Initially Displayed (TTID) of its child widget.
///
/// This widget wraps around another widget to measure and report the time it takes
/// for the child widget to be initially displayed on the screen. This method
/// allows a more accurate measurement than what the default TTID implementation
/// provides. The TTID measurement begins when the route to the widget is pushed and ends
/// when `addPostFramecallback` is triggered.
///
/// Wrap the widget you want to measure with [SentryDisplayWidget], and ensure that you
/// have set up Sentry's routing instrumentation according to the Sentry documentation.
///
/// ```dart
/// SentryDisplayWidget(
/// child: MyWidget(),
/// )
/// ```
///
/// Make sure to configure Sentry's routing instrumentation in your app by following
/// the guidelines provided in Sentry's documentation for Flutter integrations:
/// https://docs.sentry.io/platforms/flutter/integrations/routing-instrumentation/
///
/// See also:
/// - [Sentry's documentation on Flutter integrations](https://docs.sentry.io/platforms/flutter/)
/// for more information on how to integrate Sentry into your Flutter application.
class SentryDisplayWidget extends StatefulWidget {
final Widget child;
final FrameCallbackHandler _frameCallbackHandler;

SentryDisplayWidget({
super.key,
required this.child,
@visibleForTesting FrameCallbackHandler? frameCallbackHandler,
}) : _frameCallbackHandler =
frameCallbackHandler ?? DefaultFrameCallbackHandler();

@override
_SentryDisplayWidgetState createState() => _SentryDisplayWidgetState();
}

class _SentryDisplayWidgetState extends State<SentryDisplayWidget> {
@override
void initState() {
super.initState();
TimeToInitialDisplayTracker().markAsManual();

widget._frameCallbackHandler.addPostFrameCallback((_) {
TimeToInitialDisplayTracker().completeTracking();
});
}

@override
Widget build(BuildContext context) {
return widget.child;
}
}
106 changes: 80 additions & 26 deletions flutter/lib/src/navigation/sentry_navigator_observer.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
// ignore_for_file: invalid_use_of_internal_member

import 'dart:async';

import 'package:flutter/material.dart';
import 'package:flutter/widgets.dart';
import 'package:meta/meta.dart';
import '../integrations/integrations.dart';
import 'time_to_display_tracker.dart';

import '../../sentry_flutter.dart';
import '../event_processor/flutter_enricher_event_processor.dart';
Expand All @@ -19,6 +26,8 @@ typedef AdditionalInfoExtractor = Map<String, dynamic>? Function(
/// This is a navigation observer to record navigational breadcrumbs.
/// For now it only records navigation events and no gestures.
///
/// It also records Time to Initial Display (TTID).
///
/// [Route]s can always be null and their [Route.settings] can also always be null.
/// For example, if the application starts, there is no previous route.
/// The [RouteSettings] is null if a developer has not specified any
Expand All @@ -44,38 +53,38 @@ typedef AdditionalInfoExtractor = Map<String, dynamic>? Function(
/// )
/// ```
///
/// See the constructor docs for the argument documentation.
/// The option [enableAutoTransactions] is enabled by default. For every new
/// route a transaction is started. It's automatically finished after
/// [autoFinishAfter] duration or when all child spans are finished,
/// if those happen to take longer. The transaction will be set to [Scope.span]
/// if the latter is empty.
///
/// Enabling the [setRouteNameAsTransaction] option overrides the current
/// [Scope.transaction] which will also override the name of the current
/// [Scope.span]. So be careful when this is used together with performance
/// monitoring.
///
/// See also:
/// - [RouteObserver](https://api.flutter.dev/flutter/widgets/RouteObserver-class.html)
/// - [Navigating with arguments](https://flutter.dev/docs/cookbook/navigation/navigate-with-arguments)
class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
/// The option [enableAutoTransactions] is enabled by default.
/// For every new route a transaction is started. It's automatically finished
/// after [autoFinishAfter] duration or when all child spans are
/// finished, if those happen to take longer.
/// The transaction will be set to [Scope.span] if the latter is empty.
///
/// Enabling the [setRouteNameAsTransaction] option overrides the
/// current [Scope.transaction] which will also override the name of the current
/// [Scope.span]. So be careful when this is used together with performance
/// monitoring.
SentryNavigatorObserver({
Hub? hub,
bool enableAutoTransactions = true,
Duration autoFinishAfter = const Duration(seconds: 3),
bool setRouteNameAsTransaction = false,
RouteNameExtractor? routeNameExtractor,
AdditionalInfoExtractor? additionalInfoProvider,
@visibleForTesting TimeToDisplayTracker? timeToDisplayTracker,
}) : _hub = hub ?? HubAdapter(),
_enableAutoTransactions = enableAutoTransactions,
_autoFinishAfter = autoFinishAfter,
_setRouteNameAsTransaction = setRouteNameAsTransaction,
_routeNameExtractor = routeNameExtractor,
_additionalInfoProvider = additionalInfoProvider,
_native = SentryFlutter.native {
_native = SentryFlutter.native,
_timeToDisplayTracker = timeToDisplayTracker ?? TimeToDisplayTracker() {
if (enableAutoTransactions) {
// ignore: invalid_use_of_internal_member
_hub.options.sdk.addIntegration('UINavigationTracing');
}
}
Expand All @@ -87,6 +96,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
final RouteNameExtractor? _routeNameExtractor;
final AdditionalInfoExtractor? _additionalInfoProvider;
final SentryNative? _native;
final TimeToDisplayTracker? _timeToDisplayTracker;

ISentrySpan? _transaction;

Expand All @@ -95,6 +105,12 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
@internal
static String? get currentRouteName => _currentRouteName;

Completer<void>? _completedDisplayTracking;

// Since didPush does not have a future, we can keep track of when the display tracking has finished
@visibleForTesting
Completer<void>? get completedDisplayTracking => _completedDisplayTracking;

@override
void didPush(Route<dynamic> route, Route<dynamic>? previousRoute) {
super.didPush(route, previousRoute);
Expand All @@ -108,8 +124,8 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
to: route.settings,
);

_finishTransaction();
_startTransaction(route);
_finishTimeToDisplayTracking();
_startTimeToDisplayTracking(route);
}

@override
Expand Down Expand Up @@ -139,8 +155,7 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
to: previousRoute?.settings,
);

_finishTransaction();
_startTransaction(previousRoute);
_finishTimeToDisplayTracking();
}

void _addBreadcrumb({
Expand All @@ -152,7 +167,6 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
navigationType: type,
from: _routeNameExtractor?.call(from) ?? from,
to: _routeNameExtractor?.call(to) ?? to,
// ignore: invalid_use_of_internal_member
timestamp: _hub.options.clock(),
data: _additionalInfoProvider?.call(from, to),
));
Expand All @@ -179,11 +193,8 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
}
}

Future<void> _startTransaction(Route<dynamic>? route) async {
if (!_enableAutoTransactions) {
return;
}

Future<void> _startTransaction(
Route<dynamic>? route, DateTime startTimestamp) async {
String? name = _getRouteName(route);
final arguments = route?.settings.arguments;

Expand All @@ -196,14 +207,14 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
}
final transactionContext = SentryTransactionContext(
name,
'navigation',
SentrySpanOperations.uiLoad,
transactionNameSource: SentryTransactionNameSource.component,
// ignore: invalid_use_of_internal_member
origin: SentryTraceOrigins.autoNavigationRouteObserver,
);

_transaction = _hub.startTransactionWithContext(
transactionContext,
startTimestamp: startTimestamp,
waitForChildren: true,
autoFinishAfter: _autoFinishAfter,
trimEnd: true,
Expand Down Expand Up @@ -242,7 +253,9 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
await _native?.beginNativeFramesCollection();
}

Future<void> _finishTransaction() async {
Future<void> _finishTimeToDisplayTracking() async {
_timeToDisplayTracker?.clear();

final transaction = _transaction;
_transaction = null;
if (transaction == null || transaction.finished) {
Expand All @@ -251,6 +264,47 @@ class SentryNavigatorObserver extends RouteObserver<PageRoute<dynamic>> {
transaction.status ??= SpanStatus.ok();
await transaction.finish();
}

Future<void> _startTimeToDisplayTracking(Route<dynamic>? route) async {
if (!_enableAutoTransactions) {
return;
}

_completedDisplayTracking = Completer<void>();
String? routeName = _currentRouteName;
if (routeName == null) return;

DateTime startTimestamp = _hub.options.clock();
DateTime? endTimestamp;

if (routeName == '/') {
final appStartInfo = await NativeAppStartIntegration.getAppStartInfo();
if (appStartInfo == null) {
return;
}

startTimestamp = appStartInfo.start;
endTimestamp = appStartInfo.end;
}

await _startTransaction(route, startTimestamp);
final transaction = _transaction;
if (transaction == null) {
return;
}

if (routeName == '/' && endTimestamp != null) {
await _timeToDisplayTracker?.trackAppStartTTD(transaction,
startTimestamp: startTimestamp, endTimestamp: endTimestamp);
} else {
await _timeToDisplayTracker?.trackRegularRouteTTD(transaction,
startTimestamp: startTimestamp);
}

// Mark the tracking as completed and clear any temporary state.
_completedDisplayTracking?.complete();
_timeToDisplayTracker?.clear();
}
}

/// This class makes it easier to record breadcrumbs for events of Flutters
Expand Down
Loading