Skip to content

Set HTTP client breadcrumbs log level based on response status code #2847

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

Merged
merged 13 commits into from
Apr 15, 2025
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
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