From 117d988b347861588d574fcc789ecbb4f7c8bdb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Denis=20Andra=C5=A1ec?= Date: Mon, 18 Dec 2023 15:01:52 +0000 Subject: [PATCH] Add `ConnectivityIntegration` for web (#1765) --- CHANGELOG.md | 2 + .../connectivity_integration.dart | 36 ++++++++++ .../connectivity/connectivity_provider.dart | 9 +++ .../noop_connectivity_provider.dart | 17 +++++ .../web_connectivity_provider.dart | 32 +++++++++ flutter/lib/src/sentry_flutter.dart | 5 ++ .../connectivity_integration_test.dart | 56 +++++++++++++++ flutter/test/sentry_flutter_options_test.dart | 2 +- flutter/test/sentry_flutter_test.dart | 71 +++++++++++++------ 9 files changed, 206 insertions(+), 24 deletions(-) create mode 100644 flutter/lib/src/integrations/connectivity/connectivity_integration.dart create mode 100644 flutter/lib/src/integrations/connectivity/connectivity_provider.dart create mode 100644 flutter/lib/src/integrations/connectivity/noop_connectivity_provider.dart create mode 100644 flutter/lib/src/integrations/connectivity/web_connectivity_provider.dart create mode 100644 flutter/test/integrations/connectivity_integration_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index cc89efe310..8818d3b035 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ - Add option to opt out of fatal level for automatically collected errors ([#1738](https://github.com/getsentry/sentry-dart/pull/1738)) - Add `Hive` breadcrumbs ([#1773](https://github.com/getsentry/sentry-dart/pull/1773)) +- Add `ConnectivityIntegration` for web ([#1765](https://github.com/getsentry/sentry-dart/pull/1765)) + - We only get the info if online/offline on web platform. The added breadcrumb is set to either `wifi` or `none`. ### Dependencies diff --git a/flutter/lib/src/integrations/connectivity/connectivity_integration.dart b/flutter/lib/src/integrations/connectivity/connectivity_integration.dart new file mode 100644 index 0000000000..c8d0b80c86 --- /dev/null +++ b/flutter/lib/src/integrations/connectivity/connectivity_integration.dart @@ -0,0 +1,36 @@ +import 'package:meta/meta.dart'; +import '../../../sentry_flutter.dart'; +import 'connectivity_provider.dart'; + +class ConnectivityIntegration extends Integration { + Hub? _hub; + ConnectivityProvider? _connectivityProvider; + + @override + void call(Hub hub, SentryFlutterOptions options) { + _hub = hub; + _connectivityProvider = ConnectivityProvider(); + _connectivityProvider?.listen((connectivity) { + addBreadcrumb(connectivity); + }); + options.sdk.addIntegration('connectivityIntegration'); + } + + @override + void close() { + _hub = null; + _connectivityProvider?.cancel(); + } + + @internal + @visibleForTesting + void addBreadcrumb(String connectivity) { + _hub?.addBreadcrumb( + Breadcrumb( + category: 'device.connectivity', + level: SentryLevel.info, + type: 'connectivity', + data: {'connectivity': connectivity}), + ); + } +} diff --git a/flutter/lib/src/integrations/connectivity/connectivity_provider.dart b/flutter/lib/src/integrations/connectivity/connectivity_provider.dart new file mode 100644 index 0000000000..30095dda0d --- /dev/null +++ b/flutter/lib/src/integrations/connectivity/connectivity_provider.dart @@ -0,0 +1,9 @@ +import 'noop_connectivity_provider.dart' + if (dart.library.html) 'web_connectivity_provider.dart'; + +abstract class ConnectivityProvider { + factory ConnectivityProvider() => connectivityProvider(); + + void listen(void Function(String connectivity) onChange); + void cancel(); +} diff --git a/flutter/lib/src/integrations/connectivity/noop_connectivity_provider.dart b/flutter/lib/src/integrations/connectivity/noop_connectivity_provider.dart new file mode 100644 index 0000000000..4276448b7a --- /dev/null +++ b/flutter/lib/src/integrations/connectivity/noop_connectivity_provider.dart @@ -0,0 +1,17 @@ +import 'connectivity_provider.dart'; + +ConnectivityProvider connectivityProvider() { + return NoOpConnectivityProvider(); +} + +class NoOpConnectivityProvider implements ConnectivityProvider { + @override + void listen(void Function(String connectivity) onChange) { + // NoOp + } + + @override + void cancel() { + // NoOp + } +} diff --git a/flutter/lib/src/integrations/connectivity/web_connectivity_provider.dart b/flutter/lib/src/integrations/connectivity/web_connectivity_provider.dart new file mode 100644 index 0000000000..34d0e0ab42 --- /dev/null +++ b/flutter/lib/src/integrations/connectivity/web_connectivity_provider.dart @@ -0,0 +1,32 @@ +import 'dart:async'; +import 'dart:html' as html; + +import 'connectivity_provider.dart'; + +ConnectivityProvider connectivityProvider() { + return WebConnectivityProvider(); +} + +class WebConnectivityProvider implements ConnectivityProvider { + StreamSubscription? _onOnlineSub; + StreamSubscription? _onOfflineSub; + + @override + void listen(void Function(String connectivity) onChange) { + _onOnlineSub = html.window.onOnline.listen((_) { + onChange('wifi'); + }); + _onOfflineSub = html.window.onOffline.listen((_) { + onChange('none'); + }); + } + + @override + void cancel() { + _onOnlineSub?.cancel(); + _onOnlineSub = null; + + _onOfflineSub?.cancel(); + _onOfflineSub = null; + } +} diff --git a/flutter/lib/src/sentry_flutter.dart b/flutter/lib/src/sentry_flutter.dart index cef52ee564..62a9043bc9 100644 --- a/flutter/lib/src/sentry_flutter.dart +++ b/flutter/lib/src/sentry_flutter.dart @@ -9,6 +9,7 @@ import '../sentry_flutter.dart'; import 'event_processor/android_platform_exception_event_processor.dart'; import 'event_processor/flutter_exception_event_processor.dart'; import 'event_processor/platform_exception_event_processor.dart'; +import 'integrations/connectivity/connectivity_integration.dart'; import 'integrations/screenshot_integration.dart'; import 'native/factory.dart'; import 'native/native_scope_observer.dart'; @@ -171,6 +172,10 @@ mixin SentryFlutter { integrations.add(ScreenshotIntegration()); } + if (platformChecker.isWeb) { + integrations.add(ConnectivityIntegration()); + } + // works with Skia, CanvasKit and HTML renderer integrations.add(SentryViewHierarchyIntegration()); diff --git a/flutter/test/integrations/connectivity_integration_test.dart b/flutter/test/integrations/connectivity_integration_test.dart new file mode 100644 index 0000000000..2f0781e9ec --- /dev/null +++ b/flutter/test/integrations/connectivity_integration_test.dart @@ -0,0 +1,56 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/mockito.dart'; +import 'package:sentry/sentry.dart'; +import 'package:sentry_flutter/src/integrations/connectivity/connectivity_integration.dart'; +import 'package:sentry_flutter/src/sentry_flutter_options.dart'; + +import '../mocks.dart'; +import '../mocks.mocks.dart'; + +void main() { + WidgetsFlutterBinding.ensureInitialized(); + + late Fixture fixture; + + setUp(() { + fixture = Fixture(); + }); + + verifyBreadcrumb(Breadcrumb crumb, String connectivityData) { + expect(crumb.category, 'device.connectivity'); + expect(crumb.type, 'connectivity'); + expect(crumb.level, SentryLevel.info); + expect(crumb.data?['connectivity'], connectivityData); + } + + test('adds integration', () { + final sut = fixture.getSut(); + sut(fixture.hub, fixture.options); + + expect(fixture.options.sdk.integrations.contains('connectivityIntegration'), + true); + }); + + test('$ConnectivityIntegration: addsBreadcrumb', () { + final integration = fixture.getSut(); + integration.call(fixture.hub, fixture.options); + + integration.addBreadcrumb('wifi'); + + final crumb = verify( + fixture.hub.addBreadcrumb(captureAny), + ).captured.first as Breadcrumb; + + verifyBreadcrumb(crumb, 'wifi'); + }); +} + +class Fixture { + final hub = MockHub(); + final options = SentryFlutterOptions(dsn: fakeDsn); + + ConnectivityIntegration getSut() { + return ConnectivityIntegration(); + } +} diff --git a/flutter/test/sentry_flutter_options_test.dart b/flutter/test/sentry_flutter_options_test.dart index a6d871318b..ed4a3ea7f7 100644 --- a/flutter/test/sentry_flutter_options_test.dart +++ b/flutter/test/sentry_flutter_options_test.dart @@ -32,7 +32,7 @@ void main() { expect(options.enableAutoNativeBreadcrumbs, isFalse); }); - testWidgets('useFlutterBreadcrumbTracking', (WidgetTester tester) async { + testWidgets('useNativeBreadcrumbTracking', (WidgetTester tester) async { final options = SentryFlutterOptions(); options.useNativeBreadcrumbTracking(); diff --git a/flutter/test/sentry_flutter_test.dart b/flutter/test/sentry_flutter_test.dart index 87e4f46e84..f01c9a8d3f 100644 --- a/flutter/test/sentry_flutter_test.dart +++ b/flutter/test/sentry_flutter_test.dart @@ -3,6 +3,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:sentry_flutter/src/integrations/connectivity/connectivity_integration.dart'; import 'package:sentry_flutter/src/integrations/integrations.dart'; import 'package:sentry_flutter/src/integrations/screenshot_integration.dart'; import 'package:sentry_flutter/src/profiling.dart'; @@ -23,6 +24,10 @@ final platformAgnosticIntegrations = [ SentryViewHierarchyIntegration, ]; +final webIntegrations = [ + ConnectivityIntegration, +]; + final nonWebIntegrations = [ OnErrorIntegration, ]; @@ -81,14 +86,18 @@ void main() { options: sentryFlutterOptions!, expectedHasNativeScopeObserver: true); testConfiguration( - integrations: integrations, - shouldHaveIntegrations: [ - ...androidIntegrations, - ...nativeIntegrations, - ...platformAgnosticIntegrations, - ...nonWebIntegrations, - ], - shouldNotHaveIntegrations: iOsAndMacOsIntegrations); + integrations: integrations, + shouldHaveIntegrations: [ + ...androidIntegrations, + ...nativeIntegrations, + ...platformAgnosticIntegrations, + ...nonWebIntegrations, + ], + shouldNotHaveIntegrations: [ + ...iOsAndMacOsIntegrations, + ...nonWebIntegrations, + ], + ); integrations .indexWhere((element) => element is WidgetsFlutterBindingIntegration); @@ -138,7 +147,10 @@ void main() { ...platformAgnosticIntegrations, ...nonWebIntegrations, ], - shouldNotHaveIntegrations: androidIntegrations, + shouldNotHaveIntegrations: [ + ...androidIntegrations, + ...nonWebIntegrations, + ], ); testBefore( @@ -179,16 +191,15 @@ void main() { testScopeObserver( options: sentryFlutterOptions!, expectedHasNativeScopeObserver: true); - testConfiguration( - integrations: integrations, - shouldHaveIntegrations: [ - ...iOsAndMacOsIntegrations, - ...nativeIntegrations, - ...platformAgnosticIntegrations, - ...nonWebIntegrations, - ], - shouldNotHaveIntegrations: androidIntegrations, - ); + testConfiguration(integrations: integrations, shouldHaveIntegrations: [ + ...iOsAndMacOsIntegrations, + ...nativeIntegrations, + ...platformAgnosticIntegrations, + ...nonWebIntegrations, + ], shouldNotHaveIntegrations: [ + ...androidIntegrations, + ...nonWebIntegrations, + ]); testBefore( integrations: integrations, @@ -239,6 +250,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...nativeIntegrations, + ...webIntegrations, ], ); @@ -290,6 +302,7 @@ void main() { ...androidIntegrations, ...iOsAndMacOsIntegrations, ...nativeIntegrations, + ...webIntegrations, ], ); @@ -336,7 +349,10 @@ void main() { testConfiguration( integrations: integrations, - shouldHaveIntegrations: platformAgnosticIntegrations, + shouldHaveIntegrations: [ + ...platformAgnosticIntegrations, + ...webIntegrations, + ], shouldNotHaveIntegrations: [ ...androidIntegrations, ...iOsAndMacOsIntegrations, @@ -383,7 +399,10 @@ void main() { testConfiguration( integrations: integrations, - shouldHaveIntegrations: platformAgnosticIntegrations, + shouldHaveIntegrations: [ + ...platformAgnosticIntegrations, + ...webIntegrations, + ], shouldNotHaveIntegrations: [ ...androidIntegrations, ...iOsAndMacOsIntegrations, @@ -427,7 +446,10 @@ void main() { testConfiguration( integrations: integrations, - shouldHaveIntegrations: platformAgnosticIntegrations, + shouldHaveIntegrations: [ + ...platformAgnosticIntegrations, + ...webIntegrations, + ], shouldNotHaveIntegrations: [ ...androidIntegrations, ...iOsAndMacOsIntegrations, @@ -472,7 +494,10 @@ void main() { testConfiguration( integrations: integrations, - shouldHaveIntegrations: platformAgnosticIntegrations, + shouldHaveIntegrations: [ + ...platformAgnosticIntegrations, + ...webIntegrations, + ], shouldNotHaveIntegrations: [ ...androidIntegrations, ...iOsAndMacOsIntegrations,