diff --git a/CHANGELOG.md b/CHANGELOG.md index d7c200eb00..b56eb929fe 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,11 @@ - Tag all spans with thread info on non-web platforms ([#3101](https://github.com/getsentry/sentry-dart/pull/3101), [#3144](https://github.com/getsentry/sentry-dart/pull/3144)) - feat(feedback): Add option to disable keyboard resize ([#3154](https://github.com/getsentry/sentry-dart/pull/3154)) +### Enhancements + +- Add `DioException` response data to error breadcrumb ([#3164](https://github.com/getsentry/sentry-dart/pull/3164)) + - Bumped `dio` min verion to `5.2.0` + ## 9.7.0-beta.1 ### Features diff --git a/packages/dio/lib/src/breadcrumb_client_adapter.dart b/packages/dio/lib/src/breadcrumb_client_adapter.dart index b7a9f07b07..ecb7f06a84 100644 --- a/packages/dio/lib/src/breadcrumb_client_adapter.dart +++ b/packages/dio/lib/src/breadcrumb_client_adapter.dart @@ -36,6 +36,7 @@ class BreadcrumbClientAdapter implements HttpClientAdapter { final stopwatch = Stopwatch(); stopwatch.start(); + DioException? dioException; try { final response = await _client.fetch(options, requestStream, cancelFuture); @@ -46,6 +47,10 @@ class BreadcrumbClientAdapter implements HttpClientAdapter { responseBodySize = HttpHeaderUtils.getContentLength(response.headers); return response; + } on DioException catch (e) { + requestHadException = true; + dioException = e; + rethrow; } catch (_) { requestHadException = true; rethrow; @@ -57,8 +62,18 @@ class BreadcrumbClientAdapter implements HttpClientAdapter { HttpSanitizer.sanitizeUrl(options.uri.toString()) ?? UrlDetails(); SentryLevel? level; + if (requestHadException) { level = SentryLevel.error; + final dioExceptionResponse = dioException?.response; + if (dioExceptionResponse != null) { + statusCode = dioExceptionResponse.statusCode; + reason = dioExceptionResponse.statusMessage; + // ignore: invalid_use_of_internal_member + responseBodySize = HttpHeaderUtils.getContentLength( + dioExceptionResponse.headers.map, + ); + } } else if (statusCode != null) { // ignore: invalid_use_of_internal_member level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode); diff --git a/packages/dio/pubspec.yaml b/packages/dio/pubspec.yaml index 6109d0efd0..74499cc643 100644 --- a/packages/dio/pubspec.yaml +++ b/packages/dio/pubspec.yaml @@ -18,7 +18,7 @@ platforms: web: dependencies: - dio: ^5.0.0 + dio: ^5.2.0 sentry: 9.7.0-beta.1 dev_dependencies: diff --git a/packages/dio/test/breadcrumb_client_adapter_test.dart b/packages/dio/test/breadcrumb_client_adapter_test.dart index 885d694b01..68df20e01d 100644 --- a/packages/dio/test/breadcrumb_client_adapter_test.dart +++ b/packages/dio/test/breadcrumb_client_adapter_test.dart @@ -1,5 +1,7 @@ // ignore_for_file: deprecated_member_use +import 'dart:typed_data'; + import 'package:dio/dio.dart'; import 'package:mockito/mockito.dart'; import 'package:sentry/sentry.dart'; @@ -148,6 +150,39 @@ void main() { expect(breadcrumb.data?['duration'], isNotNull); }); + test('breadcrumb gets added when DioException with response is thrown', + () async { + final sut = fixture.getSut( + DioExceptionWithResponseAdapter( + statusCode: 404, + statusMessage: 'Not Found', + headers: { + 'content-length': ['123'], + }, + ), + ); + + try { + await sut.get(''); + fail('Method did not throw'); + } on DioException catch (_) {} + + expect(fixture.hub.addBreadcrumbCalls.length, 1); + + final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb; + + expect(breadcrumb.type, 'http'); + expect(breadcrumb.data?['url'], 'https://example.com'); + expect(breadcrumb.data?['method'], 'GET'); + expect(breadcrumb.data?['http.query'], 'foo=bar'); + expect(breadcrumb.data?['http.fragment'], 'baz'); + expect(breadcrumb.level, SentryLevel.error); + expect(breadcrumb.data?['duration'], isNotNull); + expect(breadcrumb.data?['status_code'], 404); + expect(breadcrumb.data?['reason'], 'Not Found'); + expect(breadcrumb.data?['response_body_size'], 123); + }); + test('close does get called for user defined client', () async { final mockHub = MockHub(); @@ -185,8 +220,45 @@ void main() { class CloseableMockClientAdapter extends Mock implements HttpClientAdapter {} +class DioExceptionWithResponseAdapter implements HttpClientAdapter { + DioExceptionWithResponseAdapter({ + required this.statusCode, + required this.statusMessage, + required this.headers, + }); + + final int statusCode; + final String statusMessage; + final Map> headers; + + @override + Future fetch( + RequestOptions options, + Stream? requestStream, + Future? cancelFuture, + ) async { + final response = Response( + requestOptions: options, + statusCode: statusCode, + statusMessage: statusMessage, + headers: Headers.fromMap(headers), + ); + + throw DioException.badResponse( + requestOptions: options, + response: response, + statusCode: statusCode, + ); + } + + @override + void close({bool force = false}) { + // No-op for testing + } +} + class Fixture { - Dio getSut([MockHttpClientAdapter? client]) { + Dio getSut([HttpClientAdapter? client]) { final mc = client ?? getClient(); final dio = Dio( BaseOptions(baseUrl: 'https://example.com?foo=bar#baz'),