diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d1ba66404..efeef18b76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,7 @@ # Unreleased +* Feat: Add current route as transaction (#615) + # 6.1.0-alpha.2 * Bump Sentry Android SDK to [5.2.0](https://github.com/getsentry/sentry-dart/pull/594) (#594) diff --git a/dart/lib/src/enricher/web_enricher_event_processor.dart b/dart/lib/src/enricher/web_enricher_event_processor.dart index 2dd78fd166..7786ed8276 100644 --- a/dart/lib/src/enricher/web_enricher_event_processor.dart +++ b/dart/lib/src/enricher/web_enricher_event_processor.dart @@ -29,6 +29,7 @@ class WebEnricherEventProcessor extends EventProcessor { return event.copyWith( contexts: contexts, request: _getRequest(event.request), + transaction: event.transaction ?? _window.location.pathname, ); } diff --git a/dart/test/enricher/web_enricher_test.dart b/dart/test/enricher/web_enricher_test.dart index da9e0f24ae..28337b1bda 100644 --- a/dart/test/enricher/web_enricher_test.dart +++ b/dart/test/enricher/web_enricher_test.dart @@ -16,6 +16,20 @@ void main() { fixture = Fixture(); }); + test('add path as transaction if transaction is null', () async { + var enricher = fixture.getSut(); + final event = await enricher.apply(SentryEvent()); + + expect(event.transaction, isNotNull); + }); + + test("don't overwrite transaction", () async { + var enricher = fixture.getSut(); + final event = await enricher.apply(SentryEvent(transaction: 'foobar')); + + expect(event.transaction, 'foobar'); + }); + test('add request with user-agent header', () async { var enricher = fixture.getSut(); final event = await enricher.apply(SentryEvent()); diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index a707ad782d..2a60aaf177 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -465,6 +465,12 @@ class SecondaryScaffold extends StatelessWidget { Navigator.pop(context); }, ), + MaterialButton( + child: const Text('throw uncaught exception'), + onPressed: () { + throw Exception('Exception from SecondaryScaffold'); + }, + ), ], ), ), diff --git a/flutter/lib/src/navigation/sentry_navigator_observer.dart b/flutter/lib/src/navigation/sentry_navigator_observer.dart index 917bc67c08..2193ab568d 100644 --- a/flutter/lib/src/navigation/sentry_navigator_observer.dart +++ b/flutter/lib/src/navigation/sentry_navigator_observer.dart @@ -30,18 +30,25 @@ const _navigationKey = 'navigation'; /// // other parameter ... /// ) /// ``` +/// Enabling the [setRouteNameAsTransaction] option overrides the current +/// [Scope.transaction]. 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> { - SentryNavigatorObserver({Hub? hub}) : hub = hub ?? HubAdapter(); + SentryNavigatorObserver({Hub? hub, bool setRouteNameAsTransaction = false}) + : _hub = hub ?? HubAdapter(), + _setRouteNameAsTransaction = setRouteNameAsTransaction; - final Hub hub; + final Hub _hub; + final bool _setRouteNameAsTransaction; @override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); + _setCurrentRoute(route.settings.name); _addBreadcrumb( type: 'didPush', from: previousRoute?.settings, @@ -52,7 +59,7 @@ class SentryNavigatorObserver extends RouteObserver> { @override void didReplace({Route? newRoute, Route? oldRoute}) { super.didReplace(newRoute: newRoute, oldRoute: oldRoute); - + _setCurrentRoute(newRoute?.settings.name); _addBreadcrumb( type: 'didReplace', from: oldRoute?.settings, @@ -63,7 +70,7 @@ class SentryNavigatorObserver extends RouteObserver> { @override void didPop(Route route, Route? previousRoute) { super.didPop(route, previousRoute); - + _setCurrentRoute(previousRoute?.settings.name); _addBreadcrumb( type: 'didPop', from: route.settings, @@ -76,12 +83,23 @@ class SentryNavigatorObserver extends RouteObserver> { RouteSettings? from, RouteSettings? to, }) { - hub.addBreadcrumb(RouteObserverBreadcrumb( + _hub.addBreadcrumb(RouteObserverBreadcrumb( navigationType: type, from: from, to: to, )); } + + void _setCurrentRoute(String? name) { + if (name == null) { + return; + } + if (_setRouteNameAsTransaction) { + _hub.configureScope((scope) { + scope.transaction = name; + }); + } + } } /// This class makes it easier to record breadcrumbs for events of Flutters diff --git a/flutter/test/sentry_navigator_observer_test.dart b/flutter/test/sentry_navigator_observer_test.dart index fd08c2a61d..0ca5496d09 100644 --- a/flutter/test/sentry_navigator_observer_test.dart +++ b/flutter/test/sentry_navigator_observer_test.dart @@ -3,9 +3,16 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'mocks.dart'; import 'mocks.mocks.dart'; void main() { + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + group('RouteObserverBreadcrumb', () { test('happy path with string route agrument', () { const fromRouteSettings = RouteSettings( @@ -140,12 +147,12 @@ void main() { settings: settings, ); - RouteSettings routeSettings(String name, [Object? arguments]) => + RouteSettings routeSettings(String? name, [Object? arguments]) => RouteSettings(name: name, arguments: arguments); test('Test recording of Breadcrumbs', () { final hub = MockHub(); - final observer = SentryNavigatorObserver(hub: hub); + final observer = fixture.getSut(hub: hub); final to = routeSettings('to', 'foobar'); final previous = routeSettings('previous', 'foobar'); @@ -166,7 +173,7 @@ void main() { test('No arguments', () { final hub = MockHub(); - final observer = SentryNavigatorObserver(hub: hub); + final observer = fixture.getSut(hub: hub); final to = routeSettings('to'); final previous = routeSettings('previous'); @@ -187,7 +194,7 @@ void main() { test('No arguments & no name', () { final hub = MockHub(); - final observer = SentryNavigatorObserver(hub: hub); + final observer = fixture.getSut(hub: hub); final to = route(null); final previous = route(null); @@ -210,7 +217,7 @@ void main() { ); final hub = MockHub(); - final observer = SentryNavigatorObserver(hub: hub); + final observer = fixture.getSut(hub: hub); final to = route(); final previous = route(); @@ -226,5 +233,79 @@ void main() { ).data, ); }); + + test('route name as transaction', () { + final hub = _MockHub(); + final observer = fixture.getSut( + hub: hub, + setRouteNameAsTransaction: true, + ); + + final to = routeSettings('to'); + final previous = routeSettings('previous'); + + observer.didPush(route(to), route(previous)); + expect(hub.scope.transaction, 'to'); + + observer.didPop(route(to), route(previous)); + expect(hub.scope.transaction, 'previous'); + + observer.didReplace(newRoute: route(to), oldRoute: route(previous)); + expect(hub.scope.transaction, 'to'); + }); + + test('route name does nothing if null', () { + final hub = _MockHub(); + final observer = fixture.getSut( + hub: hub, + setRouteNameAsTransaction: true, + ); + + hub.scope.transaction = 'foo bar'; + + final to = routeSettings(null); + final previous = routeSettings(null); + + observer.didPush(route(to), route(previous)); + expect(hub.scope.transaction, 'foo bar'); + }); + + test('disabled route as transaction', () { + final hub = _MockHub(); + final observer = + fixture.getSut(hub: hub, setRouteNameAsTransaction: false); + + final to = routeSettings('to'); + final previous = routeSettings('previous'); + + observer.didPush(route(to), route(previous)); + expect(hub.scope.transaction, null); + + observer.didPop(route(to), route(previous)); + expect(hub.scope.transaction, null); + + observer.didReplace(newRoute: route(to), oldRoute: route(previous)); + expect(hub.scope.transaction, null); + }); }); } + +class Fixture { + SentryNavigatorObserver getSut({ + required Hub hub, + bool setRouteNameAsTransaction = false, + }) { + return SentryNavigatorObserver( + hub: hub, + setRouteNameAsTransaction: setRouteNameAsTransaction, + ); + } +} + +class _MockHub extends MockHub { + final Scope scope = Scope(SentryOptions(dsn: fakeDsn)); + @override + void configureScope(ScopeCallback? callback) { + callback?.call(scope); + } +}