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

DioEventProcessor: Append http response body #1557

Merged
merged 16 commits into from
Jul 26, 2023
Merged
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

[Trace origin](https://develop.sentry.dev/sdk/performance/trace-origin/) indicates what created a trace or a span. Not all transactions and spans contain enough information to tell whether the user or what precisely in the SDK created it. Origin solves this problem. The SDK now sends origin for transactions and spans.

- Append http response body ([#1557](https://github.com/getsentry/sentry-dart/pull/1557))
denrase marked this conversation as resolved.
Show resolved Hide resolved

## 7.8.0

### Enhancements
Expand Down
17 changes: 17 additions & 0 deletions dart/lib/src/http_client/failed_request_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ class FailedRequestClient extends BaseClient {
try {
response = await _client.send(request);
statusCode = response.statusCode;

return response;
} catch (e, st) {
exception = e;
Expand Down Expand Up @@ -210,6 +211,9 @@ class FailedRequestClient extends BaseClient {
headers: _hub.options.sendDefaultPii ? response.headers : null,
bodySize: response.contentLength,
statusCode: response.statusCode,
data: _hub.options.sendDefaultPii
? await _getDataFromStreamedResponse(response)
: null,
);
hint.set(TypeCheckHint.httpResponse, response);
}
Expand All @@ -221,6 +225,19 @@ class FailedRequestClient extends BaseClient {
);
}

Future<Object?> _getDataFromStreamedResponse(
StreamedResponse streamedResponse) async {
final contentLength = streamedResponse.contentLength;
if (contentLength == null) {
return null;
}
if (!_hub.options.maxResponseBodySize.shouldAddBody(contentLength)) {
return null;
}
var response = await Response.fromStream(streamedResponse);
marandaneto marked this conversation as resolved.
Show resolved Hide resolved
return response.body;
}

// Types of Request can be found here:
// https://pub.dev/documentation/http/latest/http/http-library.html
Object? _getDataFromRequest(BaseRequest request) {
Expand Down
52 changes: 52 additions & 0 deletions dart/test/http_client/failed_request_client_test.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

import 'package:http/http.dart';
import 'package:http/testing.dart';
import 'package:mockito/mockito.dart';
Expand Down Expand Up @@ -281,6 +283,56 @@ void main() {
}
});

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._hub.options.captureFailedRequests = true;
fixture._hub.options.sendDefaultPii = true;

for (final scenario in scenarios) {
fixture._hub.options.maxResponseBodySize = scenario.maxBodySize;
fixture.transport.reset();

final bodyBytes = List.generate(scenario.contentLength, (index) => 0);
final bodyString = utf8.decode(bodyBytes);

final sut = fixture.getSut(
client: fixture.getClient(statusCode: 401, body: bodyString),
failedRequestStatusCodes: [SentryStatusCode(401)],
);

final request = Request('GET', requestUri);
await sut.send(request);

expect(fixture.transport.calls, 1);

final eventCall = fixture.transport.events.first;
final capturedResponse = eventCall.contexts.response;
expect(capturedResponse, isNotNull);
expect(capturedResponse?.data,
scenario.shouldBeIncluded ? isNotNull : isNull);
}
});

test('request passed to hint', () async {
fixture._hub.options.captureFailedRequests = true;

Expand Down
18 changes: 18 additions & 0 deletions dio/lib/src/dio_event_processor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,24 @@ class DioEventProcessor implements EventProcessor {
headers: _options.sendDefaultPii ? headers : null,
bodySize: dioError.response?.data?.length as int?,
statusCode: response?.statusCode,
data: _getResponseData(dioError.response?.data),
);
}

/// Returns the response data, if possible according to the users settings.
Object? _getResponseData(dynamic data) {
denrase marked this conversation as resolved.
Show resolved Hide resolved
if (!_options.sendDefaultPii) {
return null;
}
if (data is String) {
if (_options.maxResponseBodySize.shouldAddBody(data.codeUnits.length)) {
return data;
}
} else if (data is List<int>) {
denrase marked this conversation as resolved.
Show resolved Hide resolved
if (_options.maxResponseBodySize.shouldAddBody(data.length)) {
return data;
}
}
return null;
}
}
141 changes: 138 additions & 3 deletions dio/test/dio_event_processor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,122 @@ 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<dynamic>(
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<dynamic>(
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(
Expand All @@ -266,12 +382,17 @@ class Fixture {
// ignore: invalid_use_of_internal_member
SentryExceptionFactory get exceptionFactory => options.exceptionFactory;

DioEventProcessor getSut({bool sendDefaultPii = false}) {
DioEventProcessor getSut({
bool sendDefaultPii = false,
bool captureFailedRequests = true,
MaxRequestBodySize maxRequestBodySize = MaxRequestBodySize.always,
MaxResponseBodySize maxResponseBodySize = MaxResponseBodySize.always,
}) {
return DioEventProcessor(
options
..sendDefaultPii = sendDefaultPii
..maxRequestBodySize = MaxRequestBodySize.always
..maxResponseBodySize = MaxResponseBodySize.always,
..maxRequestBodySize = maxRequestBodySize
..maxResponseBodySize = maxResponseBodySize,
);
}

Expand All @@ -283,3 +404,17 @@ class Fixture {
);
}
}

class MaxBodySizeTestConfig<T> {
MaxBodySizeTestConfig(
this.maxBodySize,
this.contentLength,
this.shouldBeIncluded,
);

final T maxBodySize;
final int contentLength;
final bool shouldBeIncluded;

Matcher get matcher => shouldBeIncluded ? isNotNull : isNull;
}