Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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))

### Dependencies

- Bump Cocoa SDK from v8.8.0 to v8.9.1 ([#1553](https://github.com/getsentry/sentry-dart/pull/1553))
Expand Down
74 changes: 72 additions & 2 deletions dio/lib/src/dio_event_processor.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
// ignore_for_file: deprecated_member_use

import 'dart:convert';

import 'package:dio/dio.dart';
import 'package:sentry/sentry.dart';

Expand Down Expand Up @@ -62,7 +64,7 @@ class DioEventProcessor implements EventProcessor {
}

/// Returns the request data, if possible according to the users settings.
Object? _getRequestData(dynamic data) {
Object? _getRequestData(Object? data) {
if (!_options.sendDefaultPii) {
return null;
}
Expand All @@ -87,8 +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,
dioError.requestOptions.responseType,
),
);
}

/// Returns the response data, if possible according to the users settings.
Object? _getResponseData(Object? data, ResponseType responseType) {
if (!_options.sendDefaultPii) {
return null;
}
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<int> &&
_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<int>) {
return data.length;
} else {
return null;
}
}
}
}
201 changes: 198 additions & 3 deletions dio/test/dio_event_processor_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,64 @@ void main() {

expect(processedEvent.request?.headers, <String, String>{});
});

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,
);
}
});
});

group('response', () {
Expand All @@ -140,6 +198,7 @@ void main() {

final request = requestOptions.copyWith(
method: 'POST',
responseType: ResponseType.plain,
);
final throwable = Exception();
final dioError = DioError(
Expand Down Expand Up @@ -181,6 +240,7 @@ void main() {

final request = requestOptions.copyWith(
method: 'POST',
responseType: ResponseType.plain,
);
final throwable = Exception();
final dioError = DioError(
Expand Down Expand Up @@ -211,6 +271,121 @@ void main() {
expect(processedEvent.contexts.response?.statusCode, 200);
expect(processedEvent.contexts.response?.headers, <String, String>{});
});

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,
responseType: ResponseType.bytes,
);
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,
);
}
});

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<dynamic>(
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', () {
Expand Down Expand Up @@ -266,12 +441,18 @@ 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,
..captureFailedRequests = captureFailedRequests
..maxRequestBodySize = maxRequestBodySize
..maxResponseBodySize = maxResponseBodySize,
);
}

Expand All @@ -283,3 +464,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;
}
1 change: 0 additions & 1 deletion flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@

import 'dart:async';
import 'dart:convert';
import 'dart:io' show Platform;

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
Expand Down