Skip to content
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

[PLAT-8044] Network breadcrumbs for dart:io HttpClient #116

Merged
merged 10 commits into from
Jun 9, 2022
11 changes: 7 additions & 4 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ all: format build lint test

clean:
cd packages/bugsnag_flutter && $(FLUTTER_BIN) clean --suppress-analytics
cd packages/bugsnag_flutter_http && $(FLUTTER_BIN) clean --suppress-analytics
cd packages/bugsnag_breadcrumbs_dart_io && $(FLUTTER_BIN) clean --suppress-analytics
cd packages/bugsnag_breadcrumbs_http && $(FLUTTER_BIN) clean --suppress-analytics
cd example && $(FLUTTER_BIN) clean --suppress-analytics && \
rm -rf .idea bugsnag_flutter_example.iml \
ios/{Pods,.symlinks,Podfile.lock} \
Expand All @@ -22,7 +23,8 @@ ifeq ($(VERSION),)
endif
sed -i '' "s/## TBD/## $(VERSION) ($(shell date '+%Y-%m-%d'))/" CHANGELOG.md
sed -i '' "s/^version: .*/version: $(VERSION)/" packages/bugsnag_flutter/pubspec.yaml
sed -i '' "s/^version: .*/version: $(VERSION)/" packages/bugsnag_flutter_http/pubspec.yaml
sed -i '' "s/^version: .*/version: $(VERSION)/" packages/bugsnag_breadcrumbs_dart_io/pubspec.yaml
sed -i '' "s/^version: .*/version: $(VERSION)/" packages/bugsnag_breadcrumbs_http/pubspec.yaml
sed -i '' "s/^ 'version': .*/ 'version': '$(VERSION)'/" packages/bugsnag_flutter/lib/src/client.dart

stage: clean
Expand All @@ -43,7 +45,7 @@ example:

test:
cd packages/bugsnag_flutter && $(FLUTTER_BIN) test -r expanded --suppress-analytics
cd packages/bugsnag_flutter_http && $(FLUTTER_BIN) test -r expanded --suppress-analytics
cd packages/bugsnag_breadcrumbs_http && $(FLUTTER_BIN) test -r expanded --suppress-analytics

test-fixtures: ## Build the end-to-end test fixtures
@./features/scripts/build_ios_app.sh
Expand All @@ -54,7 +56,8 @@ format:

lint:
cd packages/bugsnag_flutter && $(FLUTTER_BIN) analyze --suppress-analytics
cd packages/bugsnag_flutter_http && $(FLUTTER_BIN) analyze --suppress-analytics
cd packages/bugsnag_breadcrumbs_dart_io && $(FLUTTER_BIN) analyze --suppress-analytics
cd packages/bugsnag_breadcrumbs_http && $(FLUTTER_BIN) analyze --suppress-analytics

e2e_android_local: features/fixtures/app/build/app/outputs/flutter-apk/app-release.apk
$(HOME)/Library/Android/sdk/platform-tools/adb uninstall com.bugsnag.flutter.test.app || true
Expand Down
17 changes: 16 additions & 1 deletion example/lib/main.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
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_example/native_crashes.dart';
import 'package:bugsnag_flutter_http/bugsnag_flutter_http.dart' as http;
import 'package:bugsnag_flutter/bugsnag_flutter.dart';
import 'package:flutter/material.dart';

Expand Down Expand Up @@ -84,6 +85,16 @@ class ExampleHomeScreen extends StatelessWidget {
void _networkError() async =>
http.get(Uri.parse('https://example.invalid')).ignore();

void _networkHttpClient() async {
var client = BugsnagHttpClient();
try {
final request = await client.getUrl(Uri.parse('https://example.com'));
await request.close();
} finally {
client.close();
}
}

@override
Widget build(BuildContext context) {
return Scaffold(
Expand Down Expand Up @@ -152,6 +163,10 @@ class ExampleHomeScreen extends StatelessWidget {
onPressed: _networkError,
child: const Text('Error'),
),
ElevatedButton(
onPressed: _networkHttpClient,
child: const Text('HttpClient'),
),
],
),
),
Expand Down
7 changes: 5 additions & 2 deletions example/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,11 @@ dependencies:
# the parent directory to use the current plugin's version.
path: ../packages/bugsnag_flutter

bugsnag_flutter_http:
path: ../packages/bugsnag_flutter_http
bugsnag_breadcrumbs_dart_io:
path: ../packages/bugsnag_breadcrumbs_dart_io

bugsnag_breadcrumbs_http:
path: ../packages/bugsnag_breadcrumbs_http

# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export 'src/httpclient.dart' show BugsnagHttpClient;
33 changes: 33 additions & 0 deletions packages/bugsnag_breadcrumbs_dart_io/lib/src/breadcrumb.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import 'dart:io';

import 'package:bugsnag_flutter/bugsnag_flutter.dart';

class Breadcrumb {
static BugsnagBreadcrumb build(
String method,
Uri url,
int requestContentLength,
Stopwatch stopwatch,
HttpClientResponse? response,
) {
final responseContentLength = response?.contentLength;
final status = (response == null)
? 'error'
: response.statusCode < 400
? 'succeeded'
: 'failed';
return BugsnagBreadcrumb('HttpClient request $status',
metadata: {
'duration': stopwatch.elapsed.inMilliseconds,
'method': method,
'url': url.toString().split('?').first,
if (url.queryParameters.isNotEmpty) 'urlParams': url.queryParameters,
if (requestContentLength > 0)
'requestContentLength': requestContentLength,
if (response != null) 'status': response.statusCode,
if (responseContentLength != null)
'responseContentLength': responseContentLength,
},
type: BugsnagBreadcrumbType.request);
}
nickdowell marked this conversation as resolved.
Show resolved Hide resolved
}
160 changes: 160 additions & 0 deletions packages/bugsnag_breadcrumbs_dart_io/lib/src/httpclient.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
// ignore_for_file: annotate_overrides

import 'dart:async';
import 'dart:io';

import 'package:bugsnag_flutter/bugsnag_flutter.dart';

import 'breadcrumb.dart';

/// An HttpClient wrapper that logs requests as Bugsnag breadcrumbs.
abstract class BugsnagHttpClient implements HttpClient {
factory BugsnagHttpClient([HttpClient? inner]) => _BugsnagHttpClient(inner);
}

class _BugsnagHttpClient implements BugsnagHttpClient {
/// The wrapped client.
final HttpClient _inner;

_BugsnagHttpClient([HttpClient? inner]) : _inner = inner ?? HttpClient();

void addCredentials(
Uri url, String realm, HttpClientCredentials credentials) =>
_inner.addCredentials(url, realm, credentials);

void addProxyCredentials(String host, int port, String realm,
HttpClientCredentials credentials) =>
_inner.addProxyCredentials(host, port, realm, credentials);

void close({bool force = false}) => _inner.close(force: force);

set authenticate(
Future<bool> Function(Uri url, String scheme, String? realm)? f) =>
_inner.authenticate = f;

set authenticateProxy(
Future<bool> Function(
String host, int port, String scheme, String? realm)?
f) =>
_inner.authenticateProxy = f;

bool get autoUncompress => _inner.autoUncompress;

set autoUncompress(bool value) => _inner.autoUncompress = value;

set badCertificateCallback(
bool Function(X509Certificate cert, String host, int port)?
callback) =>
_inner.badCertificateCallback = callback;

set connectionFactory(
Future<ConnectionTask<Socket>> Function(
Uri url, String? proxyHost, int? proxyPort)?
f) =>
(_inner as dynamic).connectionFactory = f;

Duration? get connectionTimeout => _inner.connectionTimeout;

set connectionTimeout(Duration? value) => _inner.connectionTimeout = value;

set findProxy(String Function(Uri url)? f) => _inner.findProxy = f;

set keyLog(Function(String line)? callback) =>
(_inner as dynamic).keyLog = callback;

Duration get idleTimeout => _inner.idleTimeout;

set idleTimeout(Duration value) => _inner.idleTimeout = value;

int? get maxConnectionsPerHost => _inner.maxConnectionsPerHost;

set maxConnectionsPerHost(int? value) => _inner.maxConnectionsPerHost = value;

String? get userAgent => _inner.userAgent;

set userAgent(String? value) => _inner.userAgent = value;

// HTTP connection functions.

Future<HttpClientRequest> delete(String host, int port, String path) =>
_instrument('DELETE', Uri(host: host, port: port, path: path),
() => _inner.delete(host, port, path));

Future<HttpClientRequest> deleteUrl(Uri url) =>
_instrument('DELETE', url, () => _inner.deleteUrl(url));

Future<HttpClientRequest> get(String host, int port, String path) =>
_instrument('GET', Uri(host: host, port: port, path: path),
() => _inner.get(host, port, path));

Future<HttpClientRequest> getUrl(Uri url) =>
_instrument('GET', url, () => _inner.getUrl(url));

Future<HttpClientRequest> head(String host, int port, String path) =>
_instrument('HEAD', Uri(host: host, port: port, path: path),
() => _inner.head(host, port, path));

Future<HttpClientRequest> headUrl(Uri url) =>
_instrument('HEAD', url, () => _inner.headUrl(url));

Future<HttpClientRequest> open(
String method, String host, int port, String path) =>
_instrument(method, Uri(host: host, port: port, path: path),
() => _inner.open(method, host, port, path));

Future<HttpClientRequest> openUrl(String method, Uri url) =>
_instrument(method, url, () => _inner.openUrl(method, url));

Future<HttpClientRequest> patch(String host, int port, String path) =>
_instrument('PATCH', Uri(host: host, port: port, path: path),
() => _inner.patch(host, port, path));

Future<HttpClientRequest> patchUrl(Uri url) =>
_instrument('PATCH', url, () => _inner.patchUrl(url));

Future<HttpClientRequest> post(String host, int port, String path) =>
_instrument('POST', Uri(host: host, port: port, path: path),
() => _inner.post(host, port, path));

Future<HttpClientRequest> postUrl(Uri url) =>
_instrument('POST', url, () => _inner.postUrl(url));

Future<HttpClientRequest> put(String host, int port, String path) =>
_instrument('PUT', Uri(host: host, port: port, path: path),
() => _inner.put(host, port, path));

Future<HttpClientRequest> putUrl(Uri url) =>
_instrument('PUT', url, () => _inner.putUrl(url));
}

Future<HttpClientRequest> _instrument(
String method,
Uri uri,
Future<HttpClientRequest> Function() connect,
) async {
final stopwatch = Stopwatch()..start();
try {
final request = await connect();
runZoned(() async {
_leaveBreadcrumb(Breadcrumb.build(
request.method,
request.uri,
request.contentLength,
stopwatch,
await request.done,
));
});
return request;
} catch (e) {
_leaveBreadcrumb(Breadcrumb.build(method, uri, -1, stopwatch, null));
rethrow;
}
}

Future _leaveBreadcrumb(BugsnagBreadcrumb breadcrumb) async {
nickdowell marked this conversation as resolved.
Show resolved Hide resolved
await bugsnag.leaveBreadcrumb(
breadcrumb.message,
metadata: breadcrumb.metadata,
type: breadcrumb.type,
);
}
22 changes: 22 additions & 0 deletions packages/bugsnag_breadcrumbs_dart_io/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
name: bugsnag_breadcrumbs_dart_io
description: Bugsnag network breadcrumbs for dart:io's HttpClient
version: 2.0.0
homepage: https://www.bugsnag.com/
documentation: https://docs.bugsnag.com/platforms/flutter/
repository: https://github.com/bugsnag/bugsnag-flutter
issue_tracker: https://github.com/bugsnag/bugsnag-flutter/issues

publish_to: none

environment:
sdk: ">=2.15.1 <3.0.0"
flutter: ">=2.0.0"

dependencies:
flutter:
sdk: flutter
bugsnag_flutter:
path: ../bugsnag_flutter

dev_dependencies:
flutter_lints: ^1.0.0
30 changes: 30 additions & 0 deletions packages/bugsnag_breadcrumbs_http/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/

# IntelliJ related
*.iml
*.ipr
*.iws
.idea/

# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/

# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.packages
build/
10 changes: 10 additions & 0 deletions packages/bugsnag_breadcrumbs_http/.metadata
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.

version:
revision: ee4e09cce01d6f2d7f4baebd247fde02e5008851
channel: stable

project_type: package
4 changes: 4 additions & 0 deletions packages/bugsnag_breadcrumbs_http/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml

# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name: bugsnag_flutter_http
name: bugsnag_breadcrumbs_http
description: Bugsnag network breadcrumbs for https://pub.dev/packages/http
version: 2.0.0-rc6
version: 2.0.0
homepage: https://www.bugsnag.com/
documentation: https://docs.bugsnag.com/platforms/flutter/
repository: https://github.com/bugsnag/bugsnag-flutter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import 'package:bugsnag_flutter/bugsnag_flutter.dart';
import 'package:bugsnag_flutter_http/src/breadcrumb.dart';
import 'package:bugsnag_breadcrumbs_http/src/breadcrumb.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;

Expand Down