diff --git a/flutter/example/lib/auto_close_screen.dart b/flutter/example/lib/auto_close_screen.dart new file mode 100644 index 0000000000..15e8fac1fb --- /dev/null +++ b/flutter/example/lib/auto_close_screen.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:sentry/sentry.dart'; + +/// This screen is only used to demonstrate how route navigation works. +/// Init will create a child span and pop the screen after 3 seconds. +/// Afterwards the transaction should be seen on the performance page. +class AutoCloseScreen extends StatefulWidget { + const AutoCloseScreen({super.key}); + + @override + AutoCloseScreenState createState() => AutoCloseScreenState(); +} + +class AutoCloseScreenState extends State { + static const delayInSeconds = 3; + + @override + void initState() { + super.initState(); + _doComplexOperationThenClose(); + } + + Future _doComplexOperationThenClose() async { + final activeSpan = Sentry.getSpan(); + final childSpan = activeSpan?.startChild('complex operation', + description: 'running a $delayInSeconds seconds operation'); + await Future.delayed(const Duration(seconds: delayInSeconds)); + childSpan?.finish(); + // ignore: use_build_context_synchronously + Navigator.of(context).pop(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Delayed Screen'), + ), + body: const Center( + child: Text( + 'This screen will automatically close in $delayInSeconds seconds...', + textAlign: TextAlign.center, + ), + ), + ); + } +} diff --git a/flutter/example/lib/main.dart b/flutter/example/lib/main.dart index 8ff567b8cb..61c05ab741 100644 --- a/flutter/example/lib/main.dart +++ b/flutter/example/lib/main.dart @@ -18,6 +18,7 @@ import 'package:sqflite/sqflite.dart'; import 'package:universal_platform/universal_platform.dart'; import 'package:feedback/feedback.dart' as feedback; import 'package:provider/provider.dart'; +import 'auto_close_screen.dart'; import 'drift/database.dart'; import 'drift/connection/connection.dart'; import 'user_feedback_dialog.dart'; @@ -30,9 +31,14 @@ import 'package:sentry_hive/sentry_hive.dart'; const String exampleDsn = 'https://e85b375ffb9f43cf8bdf9787768149e0@o447951.ingest.sentry.io/5428562'; +/// This is an exampleUrl that will be used to demonstrate how http requests are captured. +const String exampleUrl = 'https://jsonplaceholder.typicode.com/todos/'; + const _channel = MethodChannel('example.flutter.sentry.io'); var _isIntegrationTest = false; +final GlobalKey navigatorKey = GlobalKey(); + Future main() async { await setupSentry( () => runApp( @@ -100,6 +106,7 @@ class _MyAppState extends State { create: (_) => ThemeProvider(), child: Builder( builder: (context) => MaterialApp( + navigatorKey: navigatorKey, navigatorObservers: [ SentryNavigatorObserver(), ], @@ -112,6 +119,30 @@ class _MyAppState extends State { } } +class TooltipButton extends StatelessWidget { + final String text; + final String buttonTitle; + final void Function()? onPressed; + + const TooltipButton( + {required this.onPressed, + required this.buttonTitle, + required this.text, + Key? key}) + : super(key: key); + + @override + Widget build(BuildContext context) { + return Tooltip( + message: text, + child: ElevatedButton( + onPressed: onPressed, + key: key, + child: Text(buttonTitle), + )); + } +} + class MainScaffold extends StatelessWidget { const MainScaffold({ Key? key, @@ -154,77 +185,115 @@ class MainScaffold extends StatelessWidget { child: Column( children: [ if (_isIntegrationTest) const IntegrationTestWidget(), - const Center(child: Text('Trigger an action:\n')), - // For simplicity sake we skip the web set up for now. + const Center(child: Text('Trigger an action.\n')), + const Padding( + padding: EdgeInsets.all(15), //apply padding to all four sides + child: Center( + child: Text( + 'Long press a button to see more information. (hover on web)')), + ), + TooltipButton( + onPressed: () => navigateToAutoCloseScreen(context), + text: + 'Pushes a screen and creates a transaction named \'AutoCloseScreen\' with a child span that finishes after 3 seconds. \nAfter the screen has popped the transaction can then be seen on the performance page.', + buttonTitle: 'Route Navigation Observer', + ), if (!UniversalPlatform.isWeb) - ElevatedButton( - onPressed: () => driftTest(), - child: const Text('drift'), + TooltipButton( + onPressed: driftTest, + text: + 'Executes CRUD operations on an in-memory with Drift and sends the created transaction to Sentry.', + buttonTitle: 'drift', ), - ElevatedButton( - onPressed: () => hiveTest(), - child: const Text('hive'), - ), - ElevatedButton( - onPressed: () => sqfliteTest(), - child: const Text('sqflite'), + if (!UniversalPlatform.isWeb) + TooltipButton( + onPressed: hiveTest, + text: + 'Executes CRUD operations on an in-memory with Hive and sends the created transaction to Sentry.', + buttonTitle: 'hive', + ), + TooltipButton( + onPressed: sqfliteTest, + text: + 'Executes CRUD operations on an in-memory with Hive and sends the created transaction to Sentry.', + buttonTitle: 'sqflite', ), - ElevatedButton( + TooltipButton( onPressed: () => SecondaryScaffold.openSecondaryScaffold(context), - child: const Text('Open another Scaffold'), + text: + 'Demonstrates how the router integration adds a navigation event to the breadcrumbs that can be seen when throwing an exception for example.', + buttonTitle: 'Open another Scaffold', ), - ElevatedButton( - onPressed: () => tryCatch(), - key: const Key('dart_try_catch'), - child: const Text('Dart: try catch'), + const TooltipButton( + onPressed: tryCatch, + key: Key('dart_try_catch'), + text: 'Creates a caught exception and sends it to Sentry.', + buttonTitle: 'Dart: try catch', ), - ElevatedButton( + TooltipButton( onPressed: () => Scaffold.of(context).showBottomSheet( (context) => const Text('Scaffold error'), ), - child: const Text('Flutter error : Scaffold.of()'), + text: + 'Creates an uncaught exception and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Flutter error : Scaffold.of()', ), - ElevatedButton( + TooltipButton( // Warning : not captured if a debugger is attached // https://github.com/flutter/flutter/issues/48972 onPressed: () => throw Exception('Throws onPressed'), - child: const Text('Dart: throw onPressed'), + text: + 'Creates an uncaught exception and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Dart: throw onPressed', ), - ElevatedButton( + TooltipButton( + // Warning : not captured if a debugger is attached + // https://github.com/flutter/flutter/issues/48972 onPressed: () { - // Only relevant in debug builds - // Warning : not captured if a debugger is attached - // https://github.com/flutter/flutter/issues/48972 assert(false, 'assert failure'); }, - child: const Text('Dart: assert'), + text: + 'Creates an uncaught exception and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Dart: assert', ), // Calling the SDK with an appRunner will handle errors from Futures // in SDKs runZonedGuarded onError handler - ElevatedButton( + TooltipButton( onPressed: () async => asyncThrows(), - child: const Text('Dart: async throws'), + text: + 'Creates an async uncaught exception and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Dart: async throws', ), - ElevatedButton( + TooltipButton( onPressed: () async => { await Future.microtask( () => throw StateError('Failure in a microtask'), ) }, - child: const Text('Dart: Fail in microtask.'), + text: + 'Creates an uncaught exception in a microtask and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Dart: Fail in microtask', ), - ElevatedButton( - onPressed: () async => {await compute(loop, 10)}, - child: const Text('Dart: Fail in compute'), + TooltipButton( + onPressed: () async => { + await compute(loop, 10), + }, + text: + 'Creates an uncaught exception in a compute isolate and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Dart: Fail in compute', ), - ElevatedButton( - onPressed: () => Future.delayed( - const Duration(milliseconds: 100), - () => throw Exception('Throws in Future.delayed'), - ), - child: const Text('Throws in Future.delayed'), + TooltipButton( + onPressed: () async => { + await Future.delayed( + const Duration(milliseconds: 100), + () => throw StateError('Failure in a Future.delayed'), + ), + }, + text: + 'Creates an uncaught exception in a Future.delayed and sends it to Sentry. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Throws in Future.delayed', ), - ElevatedButton( + TooltipButton( onPressed: () { // modeled after a real exception FlutterError.onError?.call(FlutterErrorDetails( @@ -242,9 +311,11 @@ class MainScaffold extends StatelessWidget { ], )); }, - child: const Text('Capture from FlutterError.onError'), + text: + 'Creates a FlutterError and passes it to FlutterError.onError callback. This demonstrates how our flutter error integration catches unhandled exceptions.', + buttonTitle: 'Capture from FlutterError.onError', ), - ElevatedButton( + TooltipButton( onPressed: () { // Only usable on Flutter >= 3.3 // and needs the following additional setup: @@ -256,41 +327,41 @@ class MainScaffold extends StatelessWidget { StackTrace.current, ); }, - child: const Text('Capture from PlatformDispatcher.onError'), + text: + 'This is only usable on Flutter >= 3.3 and requires additional setup: options.addIntegration(OnErrorIntegration());', + buttonTitle: 'Capture from PlatformDispatcher.onError', ), - ElevatedButton( - key: const Key('view hierarchy'), - onPressed: () => {}, - child: const Visibility( - visible: true, - child: Opacity( - opacity: 0.5, - child: Text('view hierarchy'), - ), - ), - ), - ElevatedButton( + TooltipButton( onPressed: () => makeWebRequest(context), - child: const Text('Dart: Web request'), - ), - ElevatedButton( - onPressed: () => showDialogWithTextAndImage(context), - child: const Text('Flutter: Load assets'), + text: + 'Attaches web request related spans to the transaction and send it to Sentry.', + buttonTitle: 'Dart: Web request', ), - ElevatedButton( + TooltipButton( + onPressed: () => makeWebRequestWithDio(context), key: const Key('dio_web_request'), - onPressed: () async => await makeWebRequestWithDio(context), - child: const Text('Dio: Web request'), + text: + 'Attaches web request related spans to the transaction and send it to Sentry.', + buttonTitle: 'Dio: Web request', ), - ElevatedButton( + + TooltipButton( + onPressed: () => showDialogWithTextAndImage(context), + text: + 'Attaches asset bundle related spans to the transaction and send it to Sentry.', + buttonTitle: 'Flutter: Load assets', + ), + TooltipButton( onPressed: () { // ignore: avoid_print print('A print breadcrumb'); Sentry.captureMessage('A message with a print() Breadcrumb'); }, - child: const Text('Record print() as breadcrumb'), + text: + 'Sends a captureMessage to Sentry with a breadcrumb created by a print() statement.', + buttonTitle: 'Record print() as breadcrumb', ), - ElevatedButton( + TooltipButton( onPressed: () { Sentry.captureMessage( 'This event has an extra tag', @@ -299,10 +370,11 @@ class MainScaffold extends StatelessWidget { }, ); }, - child: - const Text('Capture message with scope with additional tag'), + text: + 'Sends the capture message event with additional Tag to Sentry.', + buttonTitle: 'Capture message with scope with additional tag', ), - ElevatedButton( + TooltipButton( onPressed: () async { final transaction = Sentry.getSpan() ?? Sentry.startTransaction( @@ -347,9 +419,11 @@ class MainScaffold extends StatelessWidget { // findPrimeNumber(1000000); // Uncomment to see it with profiling await transaction.finish(status: const SpanStatus.ok()); }, - child: const Text('Capture transaction'), + text: + 'Creates a custom transaction, adds child spans and send them to Sentry.', + buttonTitle: 'Capture transaction', ), - ElevatedButton( + TooltipButton( onPressed: () { Sentry.captureMessage( 'This message has an attachment', @@ -365,9 +439,10 @@ class MainScaffold extends StatelessWidget { }, ); }, - child: const Text('Capture message with attachment'), + text: 'Sends the capture message with an attachment to Sentry.', + buttonTitle: 'Capture message with attachment', ), - ElevatedButton( + TooltipButton( onPressed: () { feedback.BetterFeedback.of(context) .show((feedback.UserFeedback feedback) { @@ -391,9 +466,11 @@ class MainScaffold extends StatelessWidget { ); }); }, - child: const Text('Capture message with image attachment'), + text: + 'Sends the capture message with an image attachment to Sentry.', + buttonTitle: 'Capture message with image attachment', ), - ElevatedButton( + TooltipButton( onPressed: () async { final id = await Sentry.captureMessage('UserFeedback'); // ignore: use_build_context_synchronously @@ -409,9 +486,11 @@ class MainScaffold extends StatelessWidget { }, ); }, - child: const Text('Capture User Feedback'), + text: + 'Shows a custom user feedback dialog without an ongoing event that captures and sends user feedback data to Sentry.', + buttonTitle: 'Capture User Feedback', ), - ElevatedButton( + TooltipButton( onPressed: () async { await showDialog( context: context, @@ -420,19 +499,31 @@ class MainScaffold extends StatelessWidget { }, ); }, - child: const Text('Show UserFeedback Dialog without event'), + text: '', + buttonTitle: 'Show UserFeedback Dialog without event', ), - ElevatedButton( + TooltipButton( onPressed: () { final log = Logger('Logging'); log.info('My Logging test'); }, - child: const Text('Logging'), + text: + 'Demonstrates the logging integration. log.info() will create an info event send it to Sentry.', + buttonTitle: 'Logging', ), if (UniversalPlatform.isIOS || UniversalPlatform.isMacOS) const CocoaExample(), if (UniversalPlatform.isAndroid) const AndroidExample(), - ], + ].map((widget) { + if (kIsWeb) { + // Add vertical padding to web so the tooltip doesn't obstruct the clicking of the button below. + return Padding( + padding: const EdgeInsets.only(top: 18.0, bottom: 18.0), + child: widget, + ); + } + return widget; + }).toList(), ), ), ); @@ -596,6 +687,16 @@ class AndroidExample extends StatelessWidget { } } +void navigateToAutoCloseScreen(BuildContext context) { + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: 'AutoCloseScreen'), + builder: (context) => const AutoCloseScreen(), + ), + ); +} + Future tryCatch() async { try { throw StateError('try catch'); @@ -769,7 +870,7 @@ Future makeWebRequest(BuildContext context) async { ); // We don't do any exception handling here. // In case of an exception, let it get caught and reported to Sentry - final response = await client.get(Uri.parse('https://flutter.dev/')); + final response = await client.get(Uri.parse(exampleUrl)); await transaction.finish(status: const SpanStatus.ok()); @@ -781,10 +882,6 @@ Future makeWebRequest(BuildContext context) async { // ignore: use_build_context_synchronously await showDialog( context: context, - // gets tracked if using SentryNavigatorObserver - routeSettings: const RouteSettings( - name: 'flutter.dev dialog', - ), builder: (context) { return AlertDialog( title: Text('Response ${response.statusCode}'), @@ -818,7 +915,7 @@ Future makeWebRequestWithDio(BuildContext context) async { ); Response? response; try { - response = await dio.get('https://flutter.dev/'); + response = await dio.get(exampleUrl); span.status = const SpanStatus.ok(); } catch (exception, stackTrace) { span.throwable = exception; @@ -836,10 +933,6 @@ Future makeWebRequestWithDio(BuildContext context) async { // ignore: use_build_context_synchronously await showDialog( context: context, - // gets tracked if using SentryNavigatorObserver - routeSettings: const RouteSettings( - name: 'flutter.dev dialog', - ), builder: (context) { return AlertDialog( title: Text('Response ${response?.statusCode}'),