Skip to content

Commit

Permalink
Feature: Improve Dio exception reports (#718)
Browse files Browse the repository at this point in the history
  • Loading branch information
ueman authored Feb 21, 2022
1 parent edc6acd commit f4c06af
Show file tree
Hide file tree
Showing 9 changed files with 437 additions and 46 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

* [Dio] Ref: Replace FailedRequestAdapter with FailedRequestInterceptor (#728)
* Fix: Add missing return values - dart analyzer (#742)
* Feature: Add `DioEventProcessor` which improves DioError crash reports (#718)
* Fix: Do not report duplicated packages and integrations (#760)

# 6.3.0
Expand Down
19 changes: 0 additions & 19 deletions dart/lib/src/http_client/failed_request_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -217,22 +217,3 @@ extension _ListX on List<SentryStatusCode> {
return any((element) => element.isInRange(statusCode));
}
}

extension _MaxRequestBodySizeX on MaxRequestBodySize {
bool shouldAddBody(int contentLength) {
if (this == MaxRequestBodySize.never) {
return false;
}
if (this == MaxRequestBodySize.always) {
return true;
}
if (this == MaxRequestBodySize.medium && contentLength <= 10000) {
return true;
}

if (this == MaxRequestBodySize.small && contentLength <= 4000) {
return true;
}
return false;
}
}
19 changes: 19 additions & 0 deletions dart/lib/src/protocol/max_request_body_size.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,22 @@ enum MaxRequestBodySize {
/// make sense of it
always,
}

extension MaxRequestBodySizeX on MaxRequestBodySize {
bool shouldAddBody(int contentLength) {
if (this == MaxRequestBodySize.never) {
return false;
}
if (this == MaxRequestBodySize.always) {
return true;
}
if (this == MaxRequestBodySize.medium && contentLength <= 10000) {
return true;
}

if (this == MaxRequestBodySize.small && contentLength <= 4000) {
return true;
}
return false;
}
}
4 changes: 4 additions & 0 deletions dio/lib/sentry_dio.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
library sentry_dio;

export 'src/sentry_dio_extension.dart';

// Export the processor in case people want to use it standalone.
// Normally one doesn't need to use it directly.
export 'src/dio_event_processor.dart';
137 changes: 137 additions & 0 deletions dio/lib/src/dio_event_processor.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import 'dart:async';

import 'package:dio/dio.dart';
import 'package:sentry/sentry.dart';
// ignore: implementation_imports
import 'package:sentry/src/sentry_exception_factory.dart';

/// This is an [EventProcessor], which improves crash reports of [DioError]s.
/// It adds information about [DioError.response] if present and also about
/// the inner exceptions.
class DioEventProcessor implements EventProcessor {
/// This is an [EventProcessor], which improves crash reports of [DioError]s.
DioEventProcessor(this._options, this._maxRequestBodySize);

final SentryOptions _options;
final MaxRequestBodySize _maxRequestBodySize;

SentryExceptionFactory get _sentryExceptionFactory =>
// ignore: invalid_use_of_internal_member
_options.exceptionFactory;

@override
FutureOr<SentryEvent?> apply(SentryEvent event, {dynamic hint}) {
final dynamic dioError = event.throwable;
if (dioError is! DioError) {
return event;
}

// Don't override just parts of the original request.
// Keep the original one or if there's none create one.
event = event.copyWith(request: event.request ?? _requestFrom(dioError));

final innerDioStackTrace = dioError.stackTrace;
final innerDioErrorException = dioError.error as Object?;

// If the inner errors stacktrace is null,
// there's nothing to create chained exception
if (innerDioStackTrace == null) {
return event;
}

try {
final innerException = _sentryExceptionFactory.getSentryException(
innerDioErrorException ?? 'DioError inner stacktrace',
stackTrace: innerDioStackTrace,
);

final exceptions = _removeDioErrorStackTraceFromValue(
List<SentryException>.from(event.exceptions ?? <SentryException>[]),
dioError,
);

return event.copyWith(
exceptions: [
innerException,
...exceptions,
],
);
} catch (e, stackTrace) {
_options.logger(
SentryLevel.debug,
'Could not convert DioError to SentryException',
exception: e,
stackTrace: stackTrace,
);
}
return event;
}

/// Remove the StackTrace from [dioError] so the message on Sentry looks
/// much better.
List<SentryException> _removeDioErrorStackTraceFromValue(
List<SentryException> exceptions,
DioError dioError,
) {
var dioSentryException = exceptions
.where((element) => element.type == dioError.runtimeType.toString())
.first;

final exceptionIndex = exceptions.indexOf(dioSentryException);
exceptions.remove(dioSentryException);

// Remove error and stacktrace, so that the DioError value doesn't
// include the chained exception.
dioError.stackTrace = null;
dioError.error = null;

dioSentryException = dioSentryException.copyWith(
value: dioError.toString(),
);

exceptions.insert(exceptionIndex, dioSentryException);

return exceptions;
}

SentryRequest? _requestFrom(DioError dioError) {
final options = dioError.requestOptions;
// As far as I can tell there's no way to get the uri without the query part
// so we replace it with an empty string.
final urlWithoutQuery = options.uri.replace(query: '').toString();

final query = options.uri.query.isEmpty ? null : options.uri.query;

final headers = options.headers
.map((key, dynamic value) => MapEntry(key, value?.toString() ?? ''));

return SentryRequest(
method: options.method,
headers: _options.sendDefaultPii ? headers : null,
url: urlWithoutQuery,
queryString: query,
cookies: _options.sendDefaultPii
? options.headers['Cookie']?.toString()
: null,
data: _getRequestData(dioError.response?.data),
);
}

/// Returns the request data, if possible according to the users settings.
/// Type checks are based on DIOs [ResponseType].
Object? _getRequestData(dynamic data) {
if (!_options.sendDefaultPii) {
return null;
}
if (data is String) {
if (_maxRequestBodySize.shouldAddBody(data.codeUnits.length)) {
return data;
}
} else if (data is List<int>) {
if (_maxRequestBodySize.shouldAddBody(data.length)) {
return data;
}
}
return null;
}
}
21 changes: 18 additions & 3 deletions dio/lib/src/sentry_dio_extension.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:dio/dio.dart';
import 'package:meta/meta.dart';
import 'package:sentry/sentry.dart';
import 'dio_event_processor.dart';
import 'failed_request_interceptor.dart';
import 'sentry_transformer.dart';
import 'sentry_dio_client_adapter.dart';
Expand All @@ -16,10 +17,20 @@ extension SentryDioExtension on Dio {
bool recordBreadcrumbs = true,
bool networkTracing = true,
MaxRequestBodySize maxRequestBodySize = MaxRequestBodySize.never,
List<SentryStatusCode> failedRequestStatusCodes = const [],
bool captureFailedRequests = false,
bool sendDefaultPii = false,
Hub? hub,
}) {
hub = hub ?? HubAdapter();

// ignore: invalid_use_of_internal_member
final options = hub.options;

// Add DioEventProcessor when it's not already present
if (options.eventProcessors.whereType<DioEventProcessor>().isEmpty) {
options.sdk.addIntegration('sentry_dio');
options.addEventProcessor(DioEventProcessor(options, maxRequestBodySize));
}

if (captureFailedRequests) {
// Add FailedRequestInterceptor at index 0, so it's the first interceptor.
// This ensures that it is called and not skipped by any previous interceptor.
Expand All @@ -31,9 +42,13 @@ extension SentryDioExtension on Dio {
client: httpClientAdapter,
recordBreadcrumbs: recordBreadcrumbs,
networkTracing: networkTracing,
hub: hub,
);

// intercept transformations
transformer = SentryTransformer(transformer: transformer);
transformer = SentryTransformer(
transformer: transformer,
hub: hub,
);
}
}
Loading

0 comments on commit f4c06af

Please sign in to comment.