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

Make response body accessible via hint in beforeSendTransaction callback for SentryHttpClient #2293

Open
wants to merge 33 commits into
base: v9
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2308122
capture response body for failed requests and if tracing is enabled.
martinhaintz Sep 16, 2024
da865b2
unified event capturing for request and response
martinhaintz Sep 17, 2024
bc3d263
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 4, 2024
710ab1a
make response body accessible via hint
martinhaintz Nov 5, 2024
f9abe16
fix warning
martinhaintz Nov 5, 2024
a309f16
add changelog entry
martinhaintz Nov 5, 2024
715ab7d
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 11, 2024
52ecb5e
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 13, 2024
0282579
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 18, 2024
4190467
add response body as hint for beforeTransactionSend
martinhaintz Nov 18, 2024
ac2c668
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 19, 2024
24b8ca9
update hive mocks
martinhaintz Nov 19, 2024
7042c14
update drift mocks
martinhaintz Nov 19, 2024
b9fbb3a
update dio mocks
martinhaintz Nov 19, 2024
557958f
update file mocks
martinhaintz Nov 19, 2024
5959137
update mocks for isar
martinhaintz Nov 19, 2024
b417e98
update sqflite mocks
martinhaintz Nov 19, 2024
1ead661
add script to run builder for all packages
martinhaintz Nov 19, 2024
fdadb89
fix streamed response copier
martinhaintz Nov 19, 2024
f773568
fix dart analyze
martinhaintz Nov 19, 2024
b0bca7d
fix analyzer and format
martinhaintz Nov 19, 2024
9e2ff12
update flutter mocks and fix tests
martinhaintz Nov 19, 2024
8896a1e
fix mocks
martinhaintz Nov 19, 2024
9b4000b
fix analyze options
martinhaintz Nov 19, 2024
86ffb80
cleanup and code reformating
martinhaintz Nov 19, 2024
b325b09
revert changes
martinhaintz Nov 19, 2024
b38f597
remove unused import
martinhaintz Nov 19, 2024
47f50aa
removed hint from tracer
martinhaintz Nov 20, 2024
a188061
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 20, 2024
4d43edd
Merge branch 'main' into feat/capture-http-response-body-for-sentry-h…
martinhaintz Nov 25, 2024
537230b
fix changelog example
martinhaintz Nov 25, 2024
d9c2674
fix changelog example
martinhaintz Dec 2, 2024
a5c5b8e
Merge branch 'v9' into feat/capture-http-response-body-for-sentry-htt…
martinhaintz Dec 4, 2024
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,17 @@

### Enhancements

- Make response body accessible via hint in `beforSend` callback for failed web requests or if tracing is enabled in `SentryHttpClient` ([#2293](https://github.com/getsentry/sentry-dart/pull/2293))
```dart
options.beforeSend = (event, hint) async {
final response = hint.get(TypeCheckHint.httpResponse);
if (response is StreamedResponse) {
final body = await response.stream.bytesToString();
// user can now use it
}
return event;
};
```
- Remove `sentry` frames if SDK falls back to current stack trace ([#2351](https://github.com/getsentry/sentry-dart/pull/2351))
- Flutter doesn't always provide stack traces for unhandled errors - this is normal Flutter behavior
- When no stack trace is provided (in Flutter errors, `captureException`, or `captureMessage`):
Expand Down
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export 'src/throwable_mechanism.dart';
export 'src/transport/transport.dart';
export 'src/integration.dart';
export 'src/event_processor.dart';
// ignore: invalid_export_of_internal_element
export 'src/http_client/sentry_http_client.dart';
export 'src/http_client/sentry_http_client_error.dart';
export 'src/sentry_attachment/sentry_attachment.dart';
Expand Down
104 changes: 10 additions & 94 deletions dart/lib/src/http_client/failed_request_client.dart
Original file line number Diff line number Diff line change
@@ -1,12 +1,9 @@
import 'package:http/http.dart';
import '../hint.dart';
import '../type_check_hint.dart';
import '../utils/http_deep_copy_streamed_response.dart';
import '../utils/tracing_utils.dart';
import 'sentry_http_client_error.dart';
import '../protocol.dart';
import '../hub.dart';
import '../hub_adapter.dart';
import '../throwable_mechanism.dart';
import 'sentry_http_client.dart';

/// A [http](https://pub.dev/packages/http)-package compatible HTTP client
Expand Down Expand Up @@ -100,26 +97,29 @@ class FailedRequestClient extends BaseClient {
Object? exception;
StackTrace? stackTrace;
StreamedResponse? response;
List<StreamedResponse> copiedResponses = [];

final stopwatch = Stopwatch();
stopwatch.start();

try {
response = await _client.send(request);
statusCode = response.statusCode;
return response;
copiedResponses = await deepCopyStreamedResponse(response, 2);
statusCode = copiedResponses[0].statusCode;
return copiedResponses[0];
} catch (e, st) {
exception = e;
stackTrace = st;
rethrow;
} finally {
stopwatch.stop();

await _captureEventIfNeeded(
request,
statusCode,
exception,
stackTrace,
response,
copiedResponses.isNotEmpty ? copiedResponses[1] : null,
stopwatch.elapsed,
);
}
Expand Down Expand Up @@ -148,10 +148,10 @@ class FailedRequestClient extends BaseClient {
}

final reason = 'HTTP Client Error with status code: $statusCode';
exception ??= SentryHttpClientError(reason);

await _captureEvent(
exception: exception,
await captureEvent(
_hub,
exception: exception ?? SentryHttpClientError(reason),
stackTrace: stackTrace,
request: request,
requestDuration: duration,
Expand All @@ -162,90 +162,6 @@ class FailedRequestClient extends BaseClient {

@override
void close() => _client.close();

// See https://develop.sentry.dev/sdk/event-payloads/request/
Future<void> _captureEvent({
required Object? exception,
StackTrace? stackTrace,
String? reason,
required Duration requestDuration,
required BaseRequest request,
required StreamedResponse? response,
}) async {
final sentryRequest = SentryRequest.fromUri(
method: request.method,
headers: _hub.options.sendDefaultPii ? request.headers : null,
uri: request.url,
data: _hub.options.sendDefaultPii ? _getDataFromRequest(request) : null,
// ignore: deprecated_member_use_from_same_package
other: {
'content_length': request.contentLength.toString(),
'duration': requestDuration.toString(),
},
);

final mechanism = Mechanism(
type: 'SentryHttpClient',
description: reason,
);

bool? snapshot;
if (exception is SentryHttpClientError) {
snapshot = true;
}

final throwableMechanism = ThrowableMechanism(
mechanism,
exception,
snapshot: snapshot,
);

final event = SentryEvent(
throwable: throwableMechanism,
request: sentryRequest,
timestamp: _hub.options.clock(),
);

final hint = Hint.withMap({TypeCheckHint.httpRequest: request});

if (response != null) {
event.contexts.response = SentryResponse(
headers: _hub.options.sendDefaultPii ? response.headers : null,
bodySize: response.contentLength,
statusCode: response.statusCode,
);
hint.set(TypeCheckHint.httpResponse, response);
}

await _hub.captureEvent(
event,
stackTrace: stackTrace,
hint: hint,
);
}

// Types of Request can be found here:
// https://pub.dev/documentation/http/latest/http/http-library.html
Object? _getDataFromRequest(BaseRequest request) {
final contentLength = request.contentLength;
if (contentLength == null) {
return null;
}
if (!_hub.options.maxRequestBodySize.shouldAddBody(contentLength)) {
return null;
}
if (request is MultipartRequest) {
final data = <String, String>{...request.fields};
return data;
}

if (request is Request) {
return request.body;
}

// There's nothing we can do for a StreamedRequest
return null;
}
}

extension _ListX on List<SentryStatusCode> {
Expand Down
89 changes: 87 additions & 2 deletions dart/lib/src/http_client/sentry_http_client.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import '../../sentry.dart';
import 'tracing_client.dart';
import '../hub.dart';
import '../hub_adapter.dart';
import 'breadcrumb_client.dart';
import 'failed_request_client.dart';

Expand Down Expand Up @@ -160,3 +160,88 @@
return '$_min..$_max';
}
}

@internal
// See https://develop.sentry.dev/sdk/event-payloads/request/
Future<void> captureEvent(
Hub hub, {
Object? exception,
StackTrace? stackTrace,
String? reason,
required Duration requestDuration,
required BaseRequest request,
required StreamedResponse? response,
}) async {
final sentryRequest = SentryRequest.fromUri(
method: request.method,
headers: hub.options.sendDefaultPii ? request.headers : null,
uri: request.url,
data: hub.options.sendDefaultPii ? _getDataFromRequest(hub, request) : null,
// ignore: deprecated_member_use_from_same_package
other: {
'content_length': request.contentLength.toString(),
'duration': requestDuration.toString(),
},
);

final mechanism = Mechanism(
type: 'SentryHttpClient',
description: reason,
);

bool? snapshot;
if (exception is SentryHttpClientError) {
snapshot = true;
}

final throwableMechanism = ThrowableMechanism(
mechanism,
exception,
snapshot: snapshot,
);

final event = SentryEvent(
throwable: throwableMechanism,
request: sentryRequest,
timestamp: hub.options.clock(),
);

final hint = Hint.withMap({TypeCheckHint.httpRequest: request});

if (response != null) {
event.contexts.response = SentryResponse(
headers: hub.options.sendDefaultPii ? response.headers : null,
bodySize: response.contentLength,
statusCode: response.statusCode);
hint.set(TypeCheckHint.httpResponse, response);
}

await hub.captureEvent(
event,
stackTrace: stackTrace,
hint: hint,
);
}

// Types of Request can be found here:
// https://pub.dev/documentation/http/latest/http/http-library.html
Object? _getDataFromRequest(Hub hub, BaseRequest request) {
final contentLength = request.contentLength;
if (contentLength == null) {
return null;
}
if (!hub.options.maxRequestBodySize.shouldAddBody(contentLength)) {
return null;
}
if (request is MultipartRequest) {
final data = <String, String>{...request.fields};

Check warning on line 237 in dart/lib/src/http_client/sentry_http_client.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/http_client/sentry_http_client.dart#L237

Added line #L237 was not covered by tests
return data;
}

if (request is Request) {
return request.body;
}

// There's nothing we can do for a StreamedRequest
return null;
}
33 changes: 22 additions & 11 deletions dart/lib/src/http_client/tracing_client.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
import 'package:http/http.dart';
import '../hub.dart';
import '../hub_adapter.dart';
import '../protocol.dart';
import '../sentry_trace_origins.dart';
import '../tracing.dart';
import '../utils/tracing_utils.dart';
import '../utils/http_sanitizer.dart';
import '../../sentry.dart';
import '../utils/http_deep_copy_streamed_response.dart';

/// A [http](https://pub.dev/packages/http)-package compatible HTTP client
/// which adds support to Sentry Performance feature.
Expand All @@ -21,6 +16,9 @@ class TracingClient extends BaseClient {
@override
Future<StreamedResponse> send(BaseRequest request) async {
// see https://develop.sentry.dev/sdk/performance/#header-sentry-trace
int? statusCode;
final stopwatch = Stopwatch();
stopwatch.start();

final urlDetails = HttpSanitizer.sanitizeUrl(request.url.toString());

Expand All @@ -45,6 +43,7 @@ class TracingClient extends BaseClient {
urlDetails?.applyToSpan(span);

StreamedResponse? response;
List<StreamedResponse> copiedResponses = [];
try {
if (containsTargetOrMatchesRegExp(
_hub.options.tracePropagationTargets, request.url.toString())) {
Expand Down Expand Up @@ -72,18 +71,30 @@ class TracingClient extends BaseClient {
}

response = await _client.send(request);
span?.setData('http.response.status_code', response.statusCode);
span?.setData('http.response_content_length', response.contentLength);
span?.status = SpanStatus.fromHttpStatusCode(response.statusCode);
copiedResponses = await deepCopyStreamedResponse(response, 2);
statusCode = copiedResponses[0].statusCode;
span?.setData('http.response.status_code', copiedResponses[1].statusCode);
span?.setData(
'http.response_content_length', copiedResponses[1].contentLength);
span?.status =
SpanStatus.fromHttpStatusCode(copiedResponses[1].statusCode);
} catch (exception) {
span?.throwable = exception;
span?.status = SpanStatus.internalError();

rethrow;
} finally {
await span?.finish();
stopwatch.stop();
await captureEvent(
_hub,
request: request,
requestDuration: stopwatch.elapsed,
response: copiedResponses.isNotEmpty ? copiedResponses[1] : null,
reason: 'HTTP Client Event with status code: $statusCode',
);
}
return response;
return copiedResponses[0];
}

@override
Expand Down
28 changes: 28 additions & 0 deletions dart/lib/src/utils/http_deep_copy_streamed_response.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import 'package:http/http.dart';
import 'package:meta/meta.dart';

/// Helper to deep copy the StreamedResponse of a web request
@internal
Future<List<StreamedResponse>> deepCopyStreamedResponse(
StreamedResponse originalResponse, int copies) async {
final List<int> bufferedData = [];

await for (final List<int> chunk in originalResponse.stream) {
bufferedData.addAll(chunk);
}
martinhaintz marked this conversation as resolved.
Show resolved Hide resolved

List<StreamedResponse> copiedElements = [];
for (int i = 1; i <= copies; i++) {
copiedElements.add(StreamedResponse(
Stream.fromIterable([bufferedData]),
originalResponse.statusCode,
contentLength: originalResponse.contentLength,
request: originalResponse.request,
headers: originalResponse.headers,
reasonPhrase: originalResponse.reasonPhrase,
isRedirect: originalResponse.isRedirect,
persistentConnection: originalResponse.persistentConnection,
));
}
return copiedElements;
}
Loading
Loading