Skip to content

Commit

Permalink
unified event capturing for request and response
Browse files Browse the repository at this point in the history
  • Loading branch information
martinhaintz committed Sep 17, 2024
1 parent 2308122 commit da865b2
Show file tree
Hide file tree
Showing 4 changed files with 118 additions and 132 deletions.
141 changes: 9 additions & 132 deletions dart/lib/src/http_client/failed_request_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -117,144 +117,21 @@ class FailedRequestClient extends BaseClient {
rethrow;
} finally {
stopwatch.stop();
await _captureEventIfNeeded(
request,
statusCode,
exception,
stackTrace,
copiedResponses.isNotEmpty ? copiedResponses[1] : null,
stopwatch.elapsed,
);
}
}

Future<void> _captureEventIfNeeded(
BaseRequest request,
int? statusCode,
Object? exception,
StackTrace? stackTrace,
StreamedResponse? response,
Duration duration) async {
if (!(_captureFailedRequests ?? _hub.options.captureFailedRequests)) {
return;
}

// Only check `failedRequestStatusCodes` & `failedRequestTargets` if no exception was thrown.
if (exception == null) {
if (!failedRequestStatusCodes._containsStatusCode(statusCode)) {
return;
}
if (!containsTargetOrMatchesRegExp(
failedRequestTargets, request.url.toString())) {
return;
}
await captureEvent(
_hub,
exception: exception,
stackTrace: stackTrace,
request: request,
requestDuration: stopwatch.elapsed,
response: copiedResponses.isNotEmpty ? copiedResponses[1] : null,
reason: 'HTTP Client Event with status code: $statusCode',
);
}

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

await _captureEvent(
exception: exception,
stackTrace: stackTrace,
request: request,
requestDuration: duration,
response: response,
reason: reason,
);
}

@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) {
final responseBody = await response.stream.bytesToString();
event.contexts.response = SentryResponse(
headers: _hub.options.sendDefaultPii ? response.headers : null,
bodySize: response.contentLength,
statusCode: response.statusCode,
data: _hub.options.sendDefaultPii &&
_hub.options.maxResponseBodySize
.shouldAddBody(response.contentLength!)
? responseBody
: null,
);
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
94 changes: 94 additions & 0 deletions dart/lib/src/http_client/sentry_http_client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'package:http/http.dart';
import 'package:meta/meta.dart';
import '../../sentry.dart';
import 'tracing_client.dart';
import '../hub.dart';

Check notice on line 5 in dart/lib/src/http_client/sentry_http_client.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of '../hub.dart' is unnecessary because all of the used elements are also provided by the import of '../../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.
import '../hub_adapter.dart';

Check notice on line 6 in dart/lib/src/http_client/sentry_http_client.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of '../hub_adapter.dart' is unnecessary because all of the used elements are also provided by the import of '../../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.
Expand Down Expand Up @@ -160,3 +162,95 @@ class SentryStatusCode {
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;
ThrowableMechanism? throwableMechanism;
if (exception is SentryHttpClientError) {
snapshot = true;
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) {
final responseBody = await response.stream.bytesToString();
event.contexts.response = SentryResponse(
headers: hub.options.sendDefaultPii ? response.headers : null,
bodySize: response.contentLength,
statusCode: response.statusCode,
data: hub.options.sendDefaultPii &&
hub.options.maxResponseBodySize
.shouldAddBody(response.contentLength!)
? responseBody
: null,
);
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};
return data;
}

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

// There's nothing we can do for a StreamedRequest
return null;
}
13 changes: 13 additions & 0 deletions dart/lib/src/http_client/tracing_client.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'package:http/http.dart';
import '../../sentry.dart';
import '../hub.dart';

Check notice on line 3 in dart/lib/src/http_client/tracing_client.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of '../hub.dart' is unnecessary because all of the used elements are also provided by the import of '../../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.
import '../hub_adapter.dart';

Check notice on line 4 in dart/lib/src/http_client/tracing_client.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of '../hub_adapter.dart' is unnecessary because all of the used elements are also provided by the import of '../../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.
import '../protocol.dart';

Check notice on line 5 in dart/lib/src/http_client/tracing_client.dart

View workflow job for this annotation

GitHub Actions / analyze / analyze

The import of '../protocol.dart' is unnecessary because all of the used elements are also provided by the import of '../../sentry.dart'.

Try removing the import directive. See https://dart.dev/diagnostics/unnecessary_import to learn more about this problem.
Expand All @@ -22,6 +23,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 Down Expand Up @@ -75,6 +79,7 @@ class TracingClient extends BaseClient {

response = await _client.send(request);
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);
Expand All @@ -93,6 +98,14 @@ class TracingClient extends BaseClient {
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 copiedResponses[0];
}
Expand Down
2 changes: 2 additions & 0 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,8 @@ class SentryOptions {
/// because the connection was interrupted.
/// Use with [SentryHttpClient] or `sentry_dio` integration for this to work,
/// or iOS native where it sets the value to `enableCaptureFailedRequests`.
@Deprecated(
"All Request and Responses are now logged, if `sendDefaultPii` is `true` and `maxRequestBodySize` and `maxResponseBodySize` conditions are met.")
bool captureFailedRequests = true;

/// Whether to records requests as breadcrumbs. This is on by default.
Expand Down

0 comments on commit da865b2

Please sign in to comment.