Skip to content
Merged
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ await SentryFlutter.init(
- Some SDK classes do not have `const` constructors anymore.
- The `copyWith` and `clone` methods of SDK classes were deprecated.
- Set log level to `warning` by default when `debug = true` ([#2836](https://github.com/getsentry/sentry-dart/pull/2836))
- Set HTTP client breadcrumbs log level based on response status code ([#2847](https://github.com/getsentry/sentry-dart/pull/2847))
- 5xx is mapped to `SentryLevel.error`
- 4xx is mapped to `SentryLevel.warning`
- Parent-child relationship for the PlatformExceptions and Cause ([#2803](https://github.com/getsentry/sentry-dart/pull/2803))
- Improves and changes exception grouping

Expand Down
2 changes: 2 additions & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,5 @@ export 'src/utils/http_sanitizer.dart';
export 'src/utils/tracing_utils.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/url_details.dart';
// ignore: invalid_export_of_internal_element
export 'src/utils/breadcrumb_log_level.dart';
10 changes: 9 additions & 1 deletion dart/lib/src/http_client/breadcrumb_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'package:http/http.dart';
import '../protocol.dart';
import '../hub.dart';
import '../hub_adapter.dart';
import '../utils/breadcrumb_log_level.dart';
import '../utils/url_details.dart';
import '../utils/http_sanitizer.dart';

Expand Down Expand Up @@ -80,8 +81,15 @@ class BreadcrumbClient extends BaseClient {
final urlDetails =
HttpSanitizer.sanitizeUrl(request.url.toString()) ?? UrlDetails();

SentryLevel? level;
if (requestHadException) {
level = SentryLevel.error;
} else if (statusCode != null) {
level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode);
}

var breadcrumb = Breadcrumb.http(
level: requestHadException ? SentryLevel.error : SentryLevel.info,
level: level,
url: Uri.parse(urlDetails.urlOrFallback),
method: request.method,
statusCode: statusCode,
Expand Down
16 changes: 16 additions & 0 deletions dart/lib/src/utils/breadcrumb_log_level.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import 'package:meta/meta.dart';

import '../../sentry.dart';

/// Determine a breadcrumb's log level (only `warning` or `error`) based on an HTTP status code.
@internal
SentryLevel? getBreadcrumbLogLevelFromHttpStatusCode(int statusCode) {
// NOTE: null defaults to 'info' in Sentry
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the comment.

if (statusCode >= 400 && statusCode < 500) {
return SentryLevel.warning;
} else if (statusCode >= 500 && statusCode < 600) {
return SentryLevel.error;
} else {
return null;
}
}
71 changes: 71 additions & 0 deletions dart/test/http_client/breadcrumb_client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ void main() {
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.data?['request_body_size'], isNotNull);
expect(breadcrumb.data?['response_body_size'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('GET: happy path for 404', () async {
Expand All @@ -60,6 +61,7 @@ void main() {
expect(breadcrumb.data?['status_code'], 404);
expect(breadcrumb.data?['reason'], 'NOT FOUND');
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.warning);
});

test('POST: happy path', () async {
Expand All @@ -78,6 +80,7 @@ void main() {
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 200);
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('PUT: happy path', () async {
Expand All @@ -96,6 +99,7 @@ void main() {
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 200);
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('DELETE: happy path', () async {
Expand All @@ -114,6 +118,73 @@ void main() {
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 200);
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('server error response (500)', () async {
final sut = fixture.getSut(
fixture.getClient(statusCode: 500, reason: 'INTERNAL SERVER ERROR'));

final response = await sut.get(requestUri);

expect(response.statusCode, 500);

expect(fixture.hub.addBreadcrumbCalls.length, 1);
final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb;

expect(breadcrumb.type, 'http');
expect(breadcrumb.data?['url'], 'https://example.com/path');
expect(breadcrumb.data?['method'], 'GET');
expect(breadcrumb.data?['http.query'], 'foo=bar');
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 500);
expect(breadcrumb.data?['reason'], 'INTERNAL SERVER ERROR');
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.error);
});

test('server redirect (3xx)', () async {
final sut = fixture.getSut(
fixture.getClient(statusCode: 308, reason: 'PERMANENT REDIRECT'));

final response = await sut.get(requestUri);

expect(response.statusCode, 308);

expect(fixture.hub.addBreadcrumbCalls.length, 1);
final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb;

expect(breadcrumb.type, 'http');
expect(breadcrumb.data?['url'], 'https://example.com/path');
expect(breadcrumb.data?['method'], 'GET');
expect(breadcrumb.data?['http.query'], 'foo=bar');
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 308);
expect(breadcrumb.data?['reason'], 'PERMANENT REDIRECT');
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('invalid status (>= 6xx)', () async {
final sut = fixture.getSut(
fixture.getClient(statusCode: 600, reason: 'UNKNOWN STATUS CODE'));

final response = await sut.get(requestUri);

expect(response.statusCode, 600);

expect(fixture.hub.addBreadcrumbCalls.length, 1);
final breadcrumb = fixture.hub.addBreadcrumbCalls.first.crumb;

expect(breadcrumb.type, 'http');
expect(breadcrumb.data?['url'], 'https://example.com/path');
expect(breadcrumb.data?['method'], 'GET');
expect(breadcrumb.data?['http.query'], 'foo=bar');
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 600);
expect(breadcrumb.data?['reason'], 'UNKNOWN STATUS CODE');
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

/// Tests, that in case an exception gets thrown, that
Expand Down
10 changes: 9 additions & 1 deletion dio/lib/src/breadcrumb_client_adapter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,16 @@ class BreadcrumbClientAdapter implements HttpClientAdapter {
// ignore: invalid_use_of_internal_member
HttpSanitizer.sanitizeUrl(options.uri.toString()) ?? UrlDetails();

SentryLevel? level;
if (requestHadException) {
level = SentryLevel.error;
} else if (statusCode != null) {
// ignore: invalid_use_of_internal_member
level = getBreadcrumbLogLevelFromHttpStatusCode(statusCode);
}

final breadcrumb = Breadcrumb.http(
level: requestHadException ? SentryLevel.error : SentryLevel.info,
level: level,
url: Uri.parse(urlDetails.urlOrFallback),
method: options.method,
statusCode: statusCode,
Expand Down
4 changes: 4 additions & 0 deletions dio/test/breadcrumb_client_adapter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ void main() {
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.data?['request_body_size'], isNull);
expect(breadcrumb.data?['response_body_size'], isNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('POST: happy path', () async {
Expand All @@ -55,6 +56,7 @@ void main() {
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 200);
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('PUT: happy path', () async {
Expand All @@ -73,6 +75,7 @@ void main() {
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 200);
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

test('DELETE: happy path', () async {
Expand All @@ -91,6 +94,7 @@ void main() {
expect(breadcrumb.data?['http.fragment'], 'baz');
expect(breadcrumb.data?['status_code'], 200);
expect(breadcrumb.data?['duration'], isNotNull);
expect(breadcrumb.level, SentryLevel.info);
});

/// Tests, that in case an exception gets thrown, that
Expand Down
Loading
Loading