From 4c85ee0c8231e15657d0db6844ce58ad4fcdb195 Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Fri, 5 Apr 2024 22:33:19 +0200 Subject: [PATCH 1/3] PLAT-11663 switch to new http wrappers (#237) --- .gitmodules | 6 ++ CHANGELOG.md | 4 ++ examples/flutter/lib/main.dart | 23 ++++--- examples/flutter/pubspec.yaml | 10 ++- features/breadcrumbs.feature | 29 ++++++++ .../dart_io_http_breadcrumb_scenario.dart | 24 +++++++ .../scenarios/http_breadcrumb_scenario.dart | 16 +++++ .../fixtures/app/lib/scenarios/scenarios.dart | 4 ++ features/fixtures/app/pubspec.yaml | 7 +- packages/bugsnag_flutter/lib/src/client.dart | 67 ++++++++++++++++++- packages/bugsnag_flutter_dart_io_http_client | 1 + packages/bugsnag_http_client | 1 + 12 files changed, 172 insertions(+), 20 deletions(-) create mode 100644 .gitmodules create mode 100644 features/fixtures/app/lib/scenarios/dart_io_http_breadcrumb_scenario.dart create mode 100644 features/fixtures/app/lib/scenarios/http_breadcrumb_scenario.dart create mode 160000 packages/bugsnag_flutter_dart_io_http_client create mode 160000 packages/bugsnag_http_client diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..e6779b1c --- /dev/null +++ b/.gitmodules @@ -0,0 +1,6 @@ +[submodule "packages/bugsnag_http_client"] + path = packages/bugsnag_http_client + url = git@github.com:bugsnag/bugsnag-flutter-http-client.git +[submodule "packages/bugsnag_flutter_dart_io_http_client"] + path = packages/bugsnag_flutter_dart_io_http_client + url = git@github.com:bugsnag/bugsnag-flutter-dart-io-http-client.git diff --git a/CHANGELOG.md b/CHANGELOG.md index 13f754c3..6c38aeeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ # Changelog +## TBD () + +- Added networkInstrumentation listener so that the new [http](https://pub.dev/packages/bugsnag_http_client) and [dart:io](https://pub.dev/packages/bugsnag_flutter_dart_io_http_client) wrappers can trigger network breadcrumbs [#237](https://github.com/bugsnag/bugsnag-flutter/pull/237) + ## 3.0.2 (2024-02-28) - Change the bugsnag_breadcrumbs_http http dependancy to ">=0.13.4" so that there are less strict version requirements [#235](https://github.com/bugsnag/bugsnag-flutter/pull/235) diff --git a/examples/flutter/lib/main.dart b/examples/flutter/lib/main.dart index 5516c353..f8180c5a 100644 --- a/examples/flutter/lib/main.dart +++ b/examples/flutter/lib/main.dart @@ -1,12 +1,13 @@ import 'dart:async'; - -import 'package:bugsnag_breadcrumbs_dart_io/bugsnag_breadcrumbs_dart_io.dart'; -import 'package:bugsnag_breadcrumbs_http/bugsnag_breadcrumbs_http.dart' as http; +import 'package:bugsnag_flutter_dart_io_http_client/bugsnag_flutter_dart_io_http_client.dart' as dart_io; +import 'package:bugsnag_http_client/bugsnag_http_client.dart' as http; import 'package:bugsnag_example/native_crashes.dart'; import 'package:bugsnag_flutter/bugsnag_flutter.dart'; import 'package:flutter/material.dart'; void main() async { + http.addSubscriber(bugsnag.networkInstrumentation); + dart_io.addSubscriber(bugsnag.networkInstrumentation); await bugsnag.start( // Find your API key in the settings menu of your Bugsnag dashboard apiKey: 'add_your_api_key_here', @@ -48,7 +49,7 @@ class ExampleHomeScreen extends StatelessWidget { // Unhandled exceptions will automatically be detected and reported. // They are displayed with an 'Error' severity on the dashboard. - void _unhandledFlutterError() { + void _unhandledFlutterError() async { throw Exception('Unhandled Exception'); } @@ -86,8 +87,8 @@ class ExampleHomeScreen extends StatelessWidget { void _networkError() async => http.get(Uri.parse('https://example.invalid')).ignore(); - void _networkHttpClient() async { - var client = BugsnagHttpClient(); + void _networkDartIoHttpClient() async { + var client = dart_io.HttpClient(); try { final request = await client.getUrl(Uri.parse('https://example.com')); await request.close(); @@ -154,19 +155,19 @@ class ExampleHomeScreen extends StatelessWidget { ), ElevatedButton( onPressed: _networkSuccess, - child: const Text('Success'), + child: const Text('Http Success'), ), ElevatedButton( onPressed: _networkFailure, - child: const Text('Failure'), + child: const Text('Http Failure'), ), ElevatedButton( onPressed: _networkError, - child: const Text('Error'), + child: const Text('Http Error'), ), ElevatedButton( - onPressed: _networkHttpClient, - child: const Text('HttpClient'), + onPressed: _networkDartIoHttpClient, + child: const Text('Dart Io Success'), ), ], ), diff --git a/examples/flutter/pubspec.yaml b/examples/flutter/pubspec.yaml index da025336..488d1021 100644 --- a/examples/flutter/pubspec.yaml +++ b/examples/flutter/pubspec.yaml @@ -27,17 +27,15 @@ dependencies: # the parent directory to use the current plugin's version. path: ../../packages/bugsnag_flutter - bugsnag_breadcrumbs_dart_io: - path: ../../packages/bugsnag_breadcrumbs_dart_io - - bugsnag_breadcrumbs_http: - path: ../../packages/bugsnag_breadcrumbs_http + bugsnag_http_client: 1.2.0 + bugsnag_flutter_dart_io_http_client: 1.2.0 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - http: ^0.13.4 + http: ^1.1.0 + dev_dependencies: flutter_test: diff --git a/features/breadcrumbs.feature b/features/breadcrumbs.feature index e745a182..2410e916 100644 --- a/features/breadcrumbs.feature +++ b/features/breadcrumbs.feature @@ -16,3 +16,32 @@ Feature: Start Bugsnag from Flutter And the error payload field "events.0.breadcrumbs.1.metaData.object.list.2" is true And the error payload field "events.0.breadcrumbs.1.name" equals "Manual breadcrumb" And the error payload field "events.0.breadcrumbs.1.type" equals "manual" + +Scenario: Http Wrapper Breadcrumbs + Given I run "HttpBreadcrumbScenario" + And I wait to receive an error + Then the error payload field "events" is an array with 1 elements + And the error payload field "events.0.breadcrumbs" is an array with 2 elements + And the error payload field "events.0.breadcrumbs.1.name" equals "package:http request succeeded" + And the error payload field "events.0.breadcrumbs.1.type" equals "request" + And the error payload field "events.0.breadcrumbs.1.metaData.status" equals 200 + And the error payload field "events.0.breadcrumbs.1.metaData.method" equals "GET" + And the error payload field "events.0.breadcrumbs.1.metaData.duration" is greater than 1 + And the error payload field "events.0.breadcrumbs.1.metaData.url" equals "http://www.google.com" + And the error payload field "events.0.breadcrumbs.1.metaData.responseContentLength" is greater than 1 + And the error payload field "events.0.breadcrumbs.1.metaData.urlParams" equals "test=test" + +Scenario: Dart IO Wrapper Breadcrumbs + Given I run "DartIoHttpBreadcrumbScenario" + And I wait to receive an error + Then the error payload field "events" is an array with 1 elements + And the error payload field "events.0.breadcrumbs" is an array with 2 elements + And the error payload field "events.0.breadcrumbs.1.name" equals "dart:io request succeeded" + And the error payload field "events.0.breadcrumbs.1.type" equals "request" + And the error payload field "events.0.breadcrumbs.1.metaData.status" equals 200 + And the error payload field "events.0.breadcrumbs.1.metaData.method" equals "GET" + And the error payload field "events.0.breadcrumbs.1.metaData.duration" is greater than 1 + And the error payload field "events.0.breadcrumbs.1.metaData.url" equals "http://www.google.com" + And the error payload field "events.0.breadcrumbs.1.metaData.responseContentLength" is greater than 1 + And the error payload field "events.0.breadcrumbs.1.metaData.urlParams" equals "test=test" + diff --git a/features/fixtures/app/lib/scenarios/dart_io_http_breadcrumb_scenario.dart b/features/fixtures/app/lib/scenarios/dart_io_http_breadcrumb_scenario.dart new file mode 100644 index 00000000..90286c06 --- /dev/null +++ b/features/fixtures/app/lib/scenarios/dart_io_http_breadcrumb_scenario.dart @@ -0,0 +1,24 @@ +import 'package:MazeRunner/scenarios/scenario.dart'; +import 'package:bugsnag_flutter/bugsnag_flutter.dart'; +import 'package:bugsnag_flutter_dart_io_http_client/bugsnag_flutter_dart_io_http_client.dart' as dart_io; + + +class DartIoHttpBreadcrumbScenario extends Scenario { + @override + Future run() async { + dart_io.addSubscriber(bugsnag.networkInstrumentation); + await bugsnag.start( + enabledBreadcrumbTypes: {BugsnagEnabledBreadcrumbType.state}, + endpoints: endpoints, + ); + + final client = dart_io.HttpClient(); + try { + final request = await client.getUrl(Uri.parse('http://www.google.com?test=test')); + await request.close(); + } finally { + client.close(); + } + await bugsnag.notify(Exception('DartIoHttpBreadcrumbScenario'), null); + } +} diff --git a/features/fixtures/app/lib/scenarios/http_breadcrumb_scenario.dart b/features/fixtures/app/lib/scenarios/http_breadcrumb_scenario.dart new file mode 100644 index 00000000..7f79d3ca --- /dev/null +++ b/features/fixtures/app/lib/scenarios/http_breadcrumb_scenario.dart @@ -0,0 +1,16 @@ +import 'package:MazeRunner/scenarios/scenario.dart'; +import 'package:bugsnag_flutter/bugsnag_flutter.dart'; +import 'package:bugsnag_http_client/bugsnag_http_client.dart' as http; + +class HttpBreadcrumbScenario extends Scenario { + @override + Future run() async { + http.addSubscriber(bugsnag.networkInstrumentation); + await bugsnag.start( + enabledBreadcrumbTypes: {BugsnagEnabledBreadcrumbType.state}, + endpoints: endpoints, + ); + await http.get(Uri.parse("http://www.google.com?test=test")); + await bugsnag.notify(Exception('HttpBreadcrumbScenario'), null); + } +} diff --git a/features/fixtures/app/lib/scenarios/scenarios.dart b/features/fixtures/app/lib/scenarios/scenarios.dart index 3e9171b9..664676c5 100644 --- a/features/fixtures/app/lib/scenarios/scenarios.dart +++ b/features/fixtures/app/lib/scenarios/scenarios.dart @@ -20,6 +20,8 @@ import 'scenario.dart'; import 'start_bugsnag_scenario.dart'; import 'throw_exception_scenario.dart'; import 'unhandled_exception_scenario.dart'; +import 'http_breadcrumb_scenario.dart'; +import 'dart_io_http_breadcrumb_scenario.dart'; class ScenarioInfo { const ScenarioInfo(this.name, this.init); @@ -55,4 +57,6 @@ final List> scenarios = [ ScenarioInfo('ThrowExceptionScenario', () => ThrowExceptionScenario()), ScenarioInfo( 'UnhandledExceptionScenario', () => UnhandledExceptionScenario()), + ScenarioInfo("HttpBreadcrumbScenario", () => HttpBreadcrumbScenario()), + ScenarioInfo("DartIoHttpBreadcrumbScenario", () => DartIoHttpBreadcrumbScenario()), ]; diff --git a/features/fixtures/app/pubspec.yaml b/features/fixtures/app/pubspec.yaml index b333d008..a44e7286 100644 --- a/features/fixtures/app/pubspec.yaml +++ b/features/fixtures/app/pubspec.yaml @@ -37,9 +37,12 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.2 - http: ^0.13.4 path_provider: ^2.0.15 - + http: ^1.1.0 + bugsnag_http_client: + path: ../../../packages/bugsnag_http_client + bugsnag_flutter_dart_io_http_client: + path: ../../../packages/bugsnag_flutter_dart_io_http_client dev_dependencies: flutter_test: sdk: flutter diff --git a/packages/bugsnag_flutter/lib/src/client.dart b/packages/bugsnag_flutter/lib/src/client.dart index 14c8c59c..d9e04131 100644 --- a/packages/bugsnag_flutter/lib/src/client.dart +++ b/packages/bugsnag_flutter/lib/src/client.dart @@ -21,6 +21,9 @@ final _notifier = { }; abstract class BugsnagClient { + + final Map _openNetworkRequests = {}; + /// An utility error handling function that will send reported errors to /// Bugsnag as unhandled. The [errorHandler] is suitable for use with /// common Dart error callbacks such as [runZonedGuarded] or [Future.onError]. @@ -234,6 +237,64 @@ abstract class BugsnagClient { /// Removes a previously added "on error" callback. void removeOnError(BugsnagOnErrorCallback onError); + + networkInstrumentation(dynamic data) { + try { + if (data is! Map) return; + String? status = data["status"]; + String? requestId = data["request_id"]; + if (requestId == null || status == null) return; + + if (status == "started") { + _onRequestStarted(requestId); + } else if (status == "complete") { + _onRequestComplete(requestId, data); + } + } catch (e) { + // Fail silently + } + } + + void _onRequestStarted(String requestId) { + final stopwatch = Stopwatch()..start(); + _openNetworkRequests[requestId] = stopwatch; + } + + void _onRequestComplete(String requestId, dynamic data) { + final stopwatch = _openNetworkRequests.remove(requestId); + if (stopwatch != null && data is Map) { + final duration = stopwatch.elapsedMilliseconds; + final String? clientName = data["client"]; + if (clientName == null) return; + + String params = ""; + final url = data["url"]; + final splitUrl = url.split("?"); + if (splitUrl != null && splitUrl.length > 1) { + params = splitUrl.last; + } + final int? statusCode = data["status_code"]; + if (statusCode == null) return; + + final String status = statusCode < 400 ? "succeeded" : "failed"; + // Assuming leaveBreadcrumb is a predefined method to log the event + leaveBreadcrumb("$clientName request $status", metadata: { + "duration": duration, + "method": data["http_method"], + "url": splitUrl.first, + if(params.isNotEmpty) + "urlParams": params, + if(data["request_content_length"] != null && data["request_content_length"] > 0) + "requestContentLength": data["request_content_length"], + if(data["response_content_length"] != null && data["response_content_length"] > 0) + "responseContentLength": data["response_content_length"], + "status": statusCode, + }, + type: BugsnagBreadcrumbType.request, + ); + } + } + } mixin DelegateClient implements BugsnagClient { @@ -338,7 +399,7 @@ mixin DelegateClient implements BugsnagClient { client.removeOnError(onError); } -class ChannelClient implements BugsnagClient { +class ChannelClient extends BugsnagClient { FlutterExceptionHandler? _previousFlutterOnError; ErrorCallback? _previousPlatformDispatcherOnError; @@ -572,6 +633,9 @@ class ChannelClient implements BugsnagClient { Future _deliverEvent(BugsnagEvent event) => _channel.invokeMethod('deliverEvent', event); + + @override + networkInstrumentation(data) {} } /// The primary `Client`. Typically this class is not accessed directly, and @@ -769,6 +833,7 @@ class Bugsnag extends BugsnagClient with DelegateClient { return const {}; } + } /// In order to determine where a crash happens Bugsnag needs to know which diff --git a/packages/bugsnag_flutter_dart_io_http_client b/packages/bugsnag_flutter_dart_io_http_client new file mode 160000 index 00000000..be514178 --- /dev/null +++ b/packages/bugsnag_flutter_dart_io_http_client @@ -0,0 +1 @@ +Subproject commit be5141780391ba4c68c4ceaa9585e04c8f346956 diff --git a/packages/bugsnag_http_client b/packages/bugsnag_http_client new file mode 160000 index 00000000..04a691a9 --- /dev/null +++ b/packages/bugsnag_http_client @@ -0,0 +1 @@ +Subproject commit 04a691a95bea9187242c3d2375deb9cfe6814fb7 From ae5f6bd6c0cce1754bfaf5205ffd7a442fefb45b Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Tue, 9 Apr 2024 09:38:59 +0200 Subject: [PATCH 2/3] Release v3.1.0 --- CHANGELOG.md | 2 +- packages/bugsnag_flutter/lib/src/client.dart | 2 +- packages/bugsnag_flutter/pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c38aeeb..5eaa8a23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,7 +1,7 @@ # Changelog -## TBD () +## 3.1.0 (2024-04-09) - Added networkInstrumentation listener so that the new [http](https://pub.dev/packages/bugsnag_http_client) and [dart:io](https://pub.dev/packages/bugsnag_flutter_dart_io_http_client) wrappers can trigger network breadcrumbs [#237](https://github.com/bugsnag/bugsnag-flutter/pull/237) diff --git a/packages/bugsnag_flutter/lib/src/client.dart b/packages/bugsnag_flutter/lib/src/client.dart index d9e04131..4096be65 100644 --- a/packages/bugsnag_flutter/lib/src/client.dart +++ b/packages/bugsnag_flutter/lib/src/client.dart @@ -17,7 +17,7 @@ import 'model.dart'; final _notifier = { 'name': 'Flutter Bugsnag Notifier', 'url': 'https://github.com/bugsnag/bugsnag-flutter', - 'version': '3.0.2' + 'version': '3.1.0' }; abstract class BugsnagClient { diff --git a/packages/bugsnag_flutter/pubspec.yaml b/packages/bugsnag_flutter/pubspec.yaml index bda2e0f7..8686abdd 100644 --- a/packages/bugsnag_flutter/pubspec.yaml +++ b/packages/bugsnag_flutter/pubspec.yaml @@ -1,6 +1,6 @@ name: bugsnag_flutter description: Bugsnag crash monitoring and reporting tool for Flutter apps -version: 3.0.2 +version: 3.1.0 homepage: https://www.bugsnag.com/ documentation: https://docs.bugsnag.com/platforms/flutter/ repository: https://github.com/bugsnag/bugsnag-flutter From d308c71316f2057b7971d057771e55250ec5c0c6 Mon Sep 17 00:00:00 2001 From: Richard Elms Date: Tue, 9 Apr 2024 10:20:02 +0200 Subject: [PATCH 3/3] Update CHANGELOG.md Co-authored-by: Tom Longridge --- CHANGELOG.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5eaa8a23..961dbbef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,11 @@ ## 3.1.0 (2024-04-09) -- Added networkInstrumentation listener so that the new [http](https://pub.dev/packages/bugsnag_http_client) and [dart:io](https://pub.dev/packages/bugsnag_flutter_dart_io_http_client) wrappers can trigger network breadcrumbs [#237](https://github.com/bugsnag/bugsnag-flutter/pull/237) +This release introduces a new `networkInstrumentation` listener so that the new [http](https://pub.dev/packages/bugsnag_http_client) and [dart:io](https://pub.dev/packages/bugsnag_flutter_dart_io_http_client) wrappers can trigger network breadcrumbs. It also introduces support for [dio](https://pub.dev/packages/dio). + +The previous `bugsnag_breadcrumbs_http` and `bugsnag_breadcrumbs_dart_io` packages will continue to work but will be deprecated in the next major release. + +See our [online docs](https://docs.bugsnag.com/platforms/flutter/customizing-breadcrumbs/#network-request-breadcrumbs) for full integration instructions. ## 3.0.2 (2024-02-28)