Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Unreleased

* Feat: Add current route as transaction (#615)
* Feat: Add Breadcrumbs for Flutters `debugPrint` (#618)

# 6.1.0-alpha.2

Expand Down
4 changes: 1 addition & 3 deletions dart/lib/src/default_integrations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -67,11 +67,9 @@ class RunZonedGuardedIntegration extends Integration {
_isPrinting = true;

try {
hub.addBreadcrumb(Breadcrumb(
hub.addBreadcrumb(Breadcrumb.console(
message: line,
level: SentryLevel.debug,
category: 'console',
type: 'debug',
));

parent.print(zone, line);
Expand Down
16 changes: 16 additions & 0 deletions dart/lib/src/protocol/breadcrumb.dart
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,22 @@ class Breadcrumb {
);
}

factory Breadcrumb.console({
String? message,
SentryLevel? level,
DateTime? timestamp,
Map<String, dynamic>? data,
}) {
return Breadcrumb(
message: message,
level: level,
category: 'console',
type: 'debug',
timestamp: timestamp,
data: data,
);
}

/// Describes the breadcrumb.
///
/// This field is optional and may be set to null.
Expand Down
33 changes: 33 additions & 0 deletions dart/test/protocol/breadcrumb_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -128,4 +128,37 @@ void main() {
'type': 'http',
});
});

test('Breadcrumb console ctor', () {
final breadcrumb = Breadcrumb.console(
message: 'Foo Bar',
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'console',
'type': 'debug',
'level': 'info',
});
});

test('extensive Breadcrumb console ctor', () {
final breadcrumb = Breadcrumb.console(
message: 'Foo Bar',
level: SentryLevel.error,
data: {'foo': 'bar'},
);
final json = breadcrumb.toJson();

expect(json, {
'message': 'Foo Bar',
'timestamp': formatDateAsIso8601WithMillisPrecision(breadcrumb.timestamp),
'category': 'console',
'type': 'debug',
'level': 'error',
'data': {'foo': 'bar'},
});
});
}
66 changes: 66 additions & 0 deletions flutter/lib/src/integrations/debug_print_integration.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import 'dart:async';

import 'package:flutter/foundation.dart';
import 'package:sentry/sentry.dart';

import '../sentry_flutter_options.dart';

/// Integration which intercepts Flutters [debugPrint] method.
/// If this integration is added, all calls to [debugPrint] a redirected to
/// add a [Breadcrumb]. [debugPrint] is not outputting to the console anymore!
/// This integration fixes the issue described in
/// [#492](https://github.com/getsentry/sentry-dart/issues/492).
class DebugPrintIntegration extends Integration<SentryFlutterOptions> {
DebugPrintCallback? _debugPrintBackup;
late Hub _hub;
late SentryFlutterOptions _options;

@override
FutureOr<void> call(Hub hub, SentryFlutterOptions options) {
_hub = hub;
_options = options;

final isDebug = options.platformChecker.isDebugMode();
final enablePrintBreadcrumbs = options.enablePrintBreadcrumbs;
if (isDebug || !enablePrintBreadcrumbs) {
return Future.value();
}

_debugPrintBackup = debugPrint;

// We're simply replacing debugPrint here. The default implementation is a
// a throttling system which prints using Darts print method. It's basically
// a fire and forget method which completes sometime in the future. We can't
// observe when it's done.
//
// This makes it impossible to just disable adding print() breadcrumbs
// before debugPrint is called and re-enable it after debugPrint was called.
// See the docs for more information.
// https://api.flutter.dev/flutter/foundation/debugPrint.html
debugPrint = _debugPrint;

options.sdk.addIntegration('debugPrintIntegration');
}

@override
void close() {
if (_debugPrintBackup != null) {
debugPrint = _debugPrintBackup!;
}
}

void _debugPrint(String? message, {int? wrapWidth}) {
if (message == null) {
_options.logger(
SentryLevel.debug,
'debugPrint Integration received "null" as message. '
'The message is dropped.',
);
return;
}
_hub.addBreadcrumb(Breadcrumb.console(
message: message,
level: SentryLevel.debug,
));
}
}
3 changes: 3 additions & 0 deletions flutter/lib/src/sentry_flutter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry/sentry.dart';

import 'flutter_enricher_event_processor.dart';
import 'integrations/debug_print_integration.dart';
import 'sentry_flutter_options.dart';

import 'default_integrations.dart';
Expand Down Expand Up @@ -107,6 +108,8 @@ mixin SentryFlutter {
integrations.add(LoadAndroidImageListIntegration(channel));
}

integrations.add(DebugPrintIntegration());

// This is an Integration because we want to execute it after all the
// error handlers are in place. Calling a MethodChannel might result
// in errors.
Expand Down
106 changes: 106 additions & 0 deletions flutter/test/integrations/debug_print_integration_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry_flutter/src/integrations/debug_print_integration.dart';

import '../mocks.dart';
import '../mocks.mocks.dart';

void main() {
late Fixture fixture;

setUp(() {
fixture = Fixture();
});

tearDown(() {
debugPrint = debugPrintSynchronously;
});

test('$DebugPrintIntegration: debugPrint adds a breadcrumb', () {
final integration = fixture.getSut();
integration.call(fixture.hub, fixture.getOptions());

debugPrint('Foo Bar');

final breadcrumb = verify(
fixture.hub.addBreadcrumb(captureAny),
).captured.first as Breadcrumb;

expect(breadcrumb.message, 'Foo Bar');
});

test(
'$DebugPrintIntegration: debugPrint does not add a breadcrumb after close',
() {
final integration = fixture.getSut();
integration.call(fixture.hub, fixture.getOptions());
integration.close();

debugPrint('Foo Bar');

verifyNever(fixture.hub.addBreadcrumb(captureAny));
});

test(
'$DebugPrintIntegration: close changes debugPrint back to default implementation',
() {
final original = debugPrint;

final integration = fixture.getSut();
integration.call(fixture.hub, fixture.getOptions());
integration.close();

expect(debugPrint, original);
});

test('$DebugPrintIntegration: disabled in debug builds', () {
final integration = fixture.getSut();
integration.call(fixture.hub, fixture.getOptions(debug: true));

debugPrint('Foo Bar');

verifyNever(fixture.hub.addBreadcrumb(captureAny));
});

test('$DebugPrintIntegration: disabled if enablePrintBreadcrumbs = false',
() {
final integration = fixture.getSut();
integration.call(
fixture.hub,
fixture.getOptions(enablePrintBreadcrumbs: false),
);

debugPrint('Foo Bar');

verifyNever(fixture.hub.addBreadcrumb(captureAny));
});

test(
'$DebugPrintIntegration: close works if debugPrintIntegration.call was not called',
() {
final integration = fixture.getSut();

// test is successful if no exception is thrown
expect(() => integration.close(), returnsNormally);
});
}

class Fixture {
final hub = MockHub();

SentryFlutterOptions getOptions({
bool debug = false,
bool enablePrintBreadcrumbs = true,
}) {
return SentryFlutterOptions(
dsn: fakeDsn,
checker: MockPlatformChecker(isDebug: debug),
)..enablePrintBreadcrumbs = enablePrintBreadcrumbs;
}

DebugPrintIntegration getSut() {
return DebugPrintIntegration();
}
}
2 changes: 2 additions & 0 deletions flutter/test/sentry_flutter_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:package_info_plus/package_info_plus.dart';
import 'package:sentry/sentry.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:sentry/src/platform_checker.dart';
import 'package:sentry_flutter/src/integrations/debug_print_integration.dart';
import 'package:sentry_flutter/src/version.dart';
import 'mocks.dart';
import 'sentry_flutter_util.dart';
Expand All @@ -13,6 +14,7 @@ import 'sentry_flutter_util.dart';
final platformAgnosticIntegrations = [
FlutterErrorIntegration,
LoadReleaseIntegration,
DebugPrintIntegration,
];

// These should only be added to Android
Expand Down