diff --git a/dio/lib/src/dio_event_processor.dart b/dio/lib/src/dio_event_processor.dart index 23acacd025..b6a17b8e0a 100644 --- a/dio/lib/src/dio_event_processor.dart +++ b/dio/lib/src/dio_event_processor.dart @@ -1,5 +1,7 @@ // ignore_for_file: deprecated_member_use +import 'dart:convert'; + import 'package:dio/dio.dart'; import 'package:sentry/sentry.dart'; @@ -87,26 +89,76 @@ class DioEventProcessor implements EventProcessor { return SentryResponse( headers: _options.sendDefaultPii ? headers : null, - bodySize: dioError.response?.data?.length as int?, + bodySize: _getBodySize( + dioError.response?.data, + dioError.requestOptions.responseType, + ), statusCode: response?.statusCode, - data: _getResponseData(dioError.response?.data), + data: _getResponseData( + dioError.response?.data, + dioError.requestOptions.responseType, + ), ); } /// Returns the response data, if possible according to the users settings. - Object? _getResponseData(Object? data) { + Object? _getResponseData(Object? data, ResponseType responseType) { if (!_options.sendDefaultPii) { return null; } - if (data is String) { - if (_options.maxResponseBodySize.shouldAddBody(data.codeUnits.length)) { - return data; - } - } else if (data is List) { - if (_options.maxResponseBodySize.shouldAddBody(data.length)) { - return data; - } + if (data == null) { + return null; + } + switch (responseType) { + case ResponseType.json: + final js = json.encode(data); + if (_options.maxResponseBodySize.shouldAddBody(js.codeUnits.length)) { + return data; + } + break; + case ResponseType.stream: + break; // No support for logging stream body. + case ResponseType.plain: + if (data is String && + _options.maxResponseBodySize.shouldAddBody(data.codeUnits.length)) { + return data; + } + break; + case ResponseType.bytes: + if (data is List && + _options.maxResponseBodySize.shouldAddBody(data.length)) { + return data; + } + break; } return null; } + + int? _getBodySize(Object? data, ResponseType responseType) { + if (data == null) { + return null; + } + switch (responseType) { + case ResponseType.json: + return json.encode(data).codeUnits.length; + case ResponseType.stream: + if (data is String) { + return data.length; + } else { + return null; + } + case ResponseType.plain: + if (data is String) { + return data.codeUnits.length; + } else { + return null; + } + case ResponseType.bytes: + if (data is List) { + return data.length; + } else { + return null; + } + } + } } diff --git a/dio/test/dio_event_processor_test.dart b/dio/test/dio_event_processor_test.dart index d5431844fa..d2a5531e80 100644 --- a/dio/test/dio_event_processor_test.dart +++ b/dio/test/dio_event_processor_test.dart @@ -132,6 +132,64 @@ void main() { expect(processedEvent.request?.headers, {}); }); + + test('request body is included according to $MaxResponseBodySize', + () async { + final scenarios = [ + // never + MaxBodySizeTestConfig(MaxRequestBodySize.never, 0, false), + MaxBodySizeTestConfig(MaxRequestBodySize.never, 4001, false), + MaxBodySizeTestConfig(MaxRequestBodySize.never, 10001, false), + // always + MaxBodySizeTestConfig(MaxRequestBodySize.always, 0, true), + MaxBodySizeTestConfig(MaxRequestBodySize.always, 4001, true), + MaxBodySizeTestConfig(MaxRequestBodySize.always, 10001, true), + // small + MaxBodySizeTestConfig(MaxRequestBodySize.small, 0, true), + MaxBodySizeTestConfig(MaxRequestBodySize.small, 4000, true), + MaxBodySizeTestConfig(MaxRequestBodySize.small, 4001, false), + // medium + MaxBodySizeTestConfig(MaxRequestBodySize.medium, 0, true), + MaxBodySizeTestConfig(MaxRequestBodySize.medium, 4001, true), + MaxBodySizeTestConfig(MaxRequestBodySize.medium, 10000, true), + MaxBodySizeTestConfig(MaxRequestBodySize.medium, 10001, false), + ]; + + for (final scenario in scenarios) { + final sut = fixture.getSut( + sendDefaultPii: true, + captureFailedRequests: true, + maxRequestBodySize: scenario.maxBodySize, + ); + + final data = List.generate(scenario.contentLength, (index) => 0); + final request = requestOptions.copyWith(method: 'POST', data: data); + final throwable = Exception(); + final dioError = DioError( + requestOptions: request, + response: Response( + requestOptions: request, + statusCode: 401, + data: data, + ), + ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); + final processedEvent = sut.apply(event) as SentryEvent; + final capturedRequest = processedEvent.request; + + expect(capturedRequest, isNotNull); + expect( + capturedRequest?.data, + scenario.shouldBeIncluded ? isNotNull : isNull, + ); + } + }); }); group('response', () { @@ -211,6 +269,117 @@ void main() { expect(processedEvent.contexts.response?.statusCode, 200); expect(processedEvent.contexts.response?.headers, {}); }); + + test('response body is included according to $MaxResponseBodySize', + () async { + final scenarios = [ + // never + MaxBodySizeTestConfig(MaxResponseBodySize.never, 0, false), + MaxBodySizeTestConfig(MaxResponseBodySize.never, 4001, false), + MaxBodySizeTestConfig(MaxResponseBodySize.never, 10001, false), + // always + MaxBodySizeTestConfig(MaxResponseBodySize.always, 0, true), + MaxBodySizeTestConfig(MaxResponseBodySize.always, 4001, true), + MaxBodySizeTestConfig(MaxResponseBodySize.always, 10001, true), + // small + MaxBodySizeTestConfig(MaxResponseBodySize.small, 0, true), + MaxBodySizeTestConfig(MaxResponseBodySize.small, 4000, true), + MaxBodySizeTestConfig(MaxResponseBodySize.small, 4001, false), + // medium + MaxBodySizeTestConfig(MaxResponseBodySize.medium, 0, true), + MaxBodySizeTestConfig(MaxResponseBodySize.medium, 4001, true), + MaxBodySizeTestConfig(MaxResponseBodySize.medium, 10000, true), + MaxBodySizeTestConfig(MaxResponseBodySize.medium, 10001, false), + ]; + + for (final scenario in scenarios) { + final sut = fixture.getSut( + sendDefaultPii: true, + captureFailedRequests: true, + maxResponseBodySize: scenario.maxBodySize, + ); + + final data = List.generate(scenario.contentLength, (index) => 0); + final request = requestOptions.copyWith(method: 'POST', data: data); + final throwable = Exception(); + final dioError = DioError( + requestOptions: request, + response: Response( + requestOptions: request, + statusCode: 401, + data: data, + ), + ); + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); + final processedEvent = sut.apply(event) as SentryEvent; + final capturedResponse = processedEvent.contexts.response; + + expect(capturedResponse, isNotNull); + expect( + capturedResponse?.data, + scenario.shouldBeIncluded ? isNotNull : isNull, + ); + } + }); + + test('data supports all response body types', () async { + final dataByType = { + ResponseType.plain: ['plain'], + ResponseType.bytes: [ + [1337] + ], + ResponseType.json: [ + 9001, + null, + 'string', + true, + ['list'], + {'map-key': 'map-value'}, + ] + }; + + for (final entry in dataByType.entries) { + final responseType = entry.key; + + for (final data in entry.value) { + final request = requestOptions.copyWith( + method: 'POST', + data: data, + responseType: responseType, + ); + final throwable = Exception(); + final dioError = DioError( + requestOptions: request, + response: Response( + requestOptions: request, + statusCode: 401, + data: data, + ), + ); + + final sut = fixture.getSut(sendDefaultPii: true); + + final event = SentryEvent( + throwable: throwable, + exceptions: [ + fixture.sentryError(throwable), + fixture.sentryError(dioError) + ], + ); + final processedEvent = sut.apply(event) as SentryEvent; + final capturedResponse = processedEvent.contexts.response; + + expect(capturedResponse, isNotNull); + expect(capturedResponse?.data, data); + } + } + }); }); test('$DioEventProcessor adds chained stacktraces', () { @@ -248,122 +417,6 @@ void main() { expect(processedEvent.exceptions?[1].value, exception.toString()); expect(processedEvent.exceptions?[1].stackTrace, isNotNull); }); - - test('request body is included according to $MaxResponseBodySize', () async { - final scenarios = [ - // never - MaxBodySizeTestConfig(MaxRequestBodySize.never, 0, false), - MaxBodySizeTestConfig(MaxRequestBodySize.never, 4001, false), - MaxBodySizeTestConfig(MaxRequestBodySize.never, 10001, false), - // always - MaxBodySizeTestConfig(MaxRequestBodySize.always, 0, true), - MaxBodySizeTestConfig(MaxRequestBodySize.always, 4001, true), - MaxBodySizeTestConfig(MaxRequestBodySize.always, 10001, true), - // small - MaxBodySizeTestConfig(MaxRequestBodySize.small, 0, true), - MaxBodySizeTestConfig(MaxRequestBodySize.small, 4000, true), - MaxBodySizeTestConfig(MaxRequestBodySize.small, 4001, false), - // medium - MaxBodySizeTestConfig(MaxRequestBodySize.medium, 0, true), - MaxBodySizeTestConfig(MaxRequestBodySize.medium, 4001, true), - MaxBodySizeTestConfig(MaxRequestBodySize.medium, 10000, true), - MaxBodySizeTestConfig(MaxRequestBodySize.medium, 10001, false), - ]; - - for (final scenario in scenarios) { - final sut = fixture.getSut( - sendDefaultPii: true, - captureFailedRequests: true, - maxRequestBodySize: scenario.maxBodySize, - ); - - final data = List.generate(scenario.contentLength, (index) => 0); - final request = requestOptions.copyWith(method: 'POST', data: data); - final throwable = Exception(); - final dioError = DioError( - requestOptions: request, - response: Response( - requestOptions: request, - statusCode: 401, - data: data, - ), - ); - final event = SentryEvent( - throwable: throwable, - exceptions: [ - fixture.sentryError(throwable), - fixture.sentryError(dioError) - ], - ); - final processedEvent = sut.apply(event) as SentryEvent; - final capturedRequest = processedEvent.request; - - expect(capturedRequest, isNotNull); - expect( - capturedRequest?.data, - scenario.shouldBeIncluded ? isNotNull : isNull, - ); - } - }); - - test('response body is included according to $MaxResponseBodySize', () async { - final scenarios = [ - // never - MaxBodySizeTestConfig(MaxResponseBodySize.never, 0, false), - MaxBodySizeTestConfig(MaxResponseBodySize.never, 4001, false), - MaxBodySizeTestConfig(MaxResponseBodySize.never, 10001, false), - // always - MaxBodySizeTestConfig(MaxResponseBodySize.always, 0, true), - MaxBodySizeTestConfig(MaxResponseBodySize.always, 4001, true), - MaxBodySizeTestConfig(MaxResponseBodySize.always, 10001, true), - // small - MaxBodySizeTestConfig(MaxResponseBodySize.small, 0, true), - MaxBodySizeTestConfig(MaxResponseBodySize.small, 4000, true), - MaxBodySizeTestConfig(MaxResponseBodySize.small, 4001, false), - // medium - MaxBodySizeTestConfig(MaxResponseBodySize.medium, 0, true), - MaxBodySizeTestConfig(MaxResponseBodySize.medium, 4001, true), - MaxBodySizeTestConfig(MaxResponseBodySize.medium, 10000, true), - MaxBodySizeTestConfig(MaxResponseBodySize.medium, 10001, false), - ]; - - fixture.options.captureFailedRequests = true; - - for (final scenario in scenarios) { - final sut = fixture.getSut( - sendDefaultPii: true, - captureFailedRequests: true, - maxResponseBodySize: scenario.maxBodySize, - ); - - final data = List.generate(scenario.contentLength, (index) => 0); - final request = requestOptions.copyWith(method: 'POST', data: data); - final throwable = Exception(); - final dioError = DioError( - requestOptions: request, - response: Response( - requestOptions: request, - statusCode: 401, - data: data, - ), - ); - final event = SentryEvent( - throwable: throwable, - exceptions: [ - fixture.sentryError(throwable), - fixture.sentryError(dioError) - ], - ); - final processedEvent = sut.apply(event) as SentryEvent; - final capturedResponse = processedEvent.contexts.response; - - expect(capturedResponse, isNotNull); - expect( - capturedResponse?.data, - scenario.shouldBeIncluded ? isNotNull : isNull, - ); - } - }); } final requestOptions = RequestOptions( @@ -391,6 +444,7 @@ class Fixture { return DioEventProcessor( options ..sendDefaultPii = sendDefaultPii + ..captureFailedRequests = captureFailedRequests ..maxRequestBodySize = maxRequestBodySize ..maxResponseBodySize = maxResponseBodySize, );