Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
Expand Up @@ -3,6 +3,7 @@
* Enhancement: Call `toString()` on all non-serializable fields (#528)
* Fix: Always call `Flutter.onError` in order to not swallow messages (#533)
* Bump: Android SDK to 5.1.0-beta.6 (#535)
* Feat: Collect more information for exceptions collected via `FlutterError.onError` (#538)

# 6.0.0-beta.2

Expand Down
20 changes: 20 additions & 0 deletions flutter/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,26 @@ class MainScaffold extends StatelessWidget {
onPressed: () => Future.delayed(Duration(milliseconds: 100),
() => throw Exception('Throws in Future.delayed')),
),
RaisedButton(
child: const Text('Capture from FlutterError.onError'),
onPressed: () {
// modeled after a real exception
FlutterError.onError?.call(FlutterErrorDetails(
exception: Exception('A really bad exception'),
silent: false,
context: DiagnosticsNode.message('while handling a gesture'),
library: 'gesture',
informationCollector: () => [
DiagnosticsNode.message(
'Handler: "onTap" Recognizer: TapGestureRecognizer'),
DiagnosticsNode.message(
'Handler: "onTap" Recognizer: TapGestureRecognizer'),
DiagnosticsNode.message(
'Handler: "onTap" Recognizer: TapGestureRecognizer'),
],
));
},
),
Comment on lines +149 to +168
Copy link
Contributor

Choose a reason for hiding this comment

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

the example Flutter error : Scaffold.of() already triggers a FlutterError.onError error, isnt that enough? is it common that people trigger FlutterError.onError manually too?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the example Flutter error : Scaffold.of() already triggers a FlutterError.onError error, isnt that enough?

This is just to test how it looks in the Sentry UI when it has a long trail of information, which Flutter error : Scaffold.of() doesn't have (as seen in the screenshot of the PR description). We can remove it before merging.

is it common that people trigger FlutterError.onError manually too?

No, but it's not uncommon in libraries.

RaisedButton(
child: const Text('Dart: Web request'),
onPressed: () => makeWebRequest(context),
Expand Down
22 changes: 20 additions & 2 deletions flutter/lib/src/default_integrations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,16 +44,34 @@ class FlutterErrorIntegration extends Integration<SentryFlutterOptions> {
void call(Hub hub, SentryFlutterOptions options) {
_defaultOnError = FlutterError.onError;
_integrationOnError = (FlutterErrorDetails errorDetails) async {
dynamic exception = errorDetails.exception;
final exception = errorDetails.exception;

options.logger(
SentryLevel.debug,
'Capture from onError $exception',
);

if (errorDetails.silent != true || options.reportSilentFlutterErrors) {
final context = errorDetails.context?.toDescription();

final collector = errorDetails.informationCollector?.call() ?? [];
final information =
(StringBuffer()..writeAll(collector, '\n')).toString();
final library = errorDetails.library;

// FlutterError doesn't crash the App.
final mechanism = Mechanism(type: 'FlutterError', handled: true);
final mechanism = Mechanism(
type: 'FlutterError',
handled: true,
data: {
// This is a message which should make sense if written after the
// word `thrown`:
// https://api.flutter.dev/flutter/foundation/FlutterErrorDetails/context.html
if (context != null) 'context': 'thrown $context',
if (collector.isNotEmpty) 'information': information,
if (library != null) 'library': library,
},
);
final throwableMechanism = ThrowableMechanism(mechanism, exception);

var event = SentryEvent(
Expand Down
42 changes: 41 additions & 1 deletion flutter/test/default_integrations_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ void main() {
bool silent = false,
FlutterExceptionHandler? handler,
dynamic exception,
FlutterErrorDetails? optionalDetails,
}) {
// replace default error otherwise it fails on testing
FlutterError.onError =
Expand All @@ -46,8 +47,11 @@ void main() {
final details = FlutterErrorDetails(
exception: throwable,
silent: silent,
context: DiagnosticsNode.message('while handling a gesture'),
library: 'sentry',
informationCollector: () => [DiagnosticsNode.message('foo bar')],
);
FlutterError.reportError(details);
FlutterError.reportError(optionalDetails ?? details);
}

test('FlutterError capture errors', () async {
Expand All @@ -64,9 +68,45 @@ void main() {
final throwableMechanism = event.throwableMechanism as ThrowableMechanism;
expect(throwableMechanism.mechanism.type, 'FlutterError');
expect(throwableMechanism.mechanism.handled, true);
expect(throwableMechanism.mechanism.data['library'], 'sentry');
expect(throwableMechanism.mechanism.data['context'],
'thrown while handling a gesture');
expect(throwableMechanism.mechanism.data['information'], 'foo bar');
expect(throwableMechanism.throwable, exception);
});

test('FlutterError capture errors with long FlutterErrorDetails.information',
() async {
final details = FlutterErrorDetails(
exception: StateError('error'),
silent: false,
context: DiagnosticsNode.message('while handling a gesture'),
library: 'sentry',
informationCollector: () => [
DiagnosticsNode.message('foo bar'),
DiagnosticsNode.message('Hello World!')
],
);

// exception is ignored in this case
_reportError(exception: StateError('error'), optionalDetails: details);

final event = verify(
await fixture.hub.captureEvent(captureAny),
).captured.first as SentryEvent;

expect(event.level, SentryLevel.fatal);

final throwableMechanism = event.throwableMechanism as ThrowableMechanism;
expect(throwableMechanism.mechanism.type, 'FlutterError');
expect(throwableMechanism.mechanism.handled, true);
expect(throwableMechanism.mechanism.data['library'], 'sentry');
expect(throwableMechanism.mechanism.data['context'],
'thrown while handling a gesture');
expect(throwableMechanism.mechanism.data['information'],
'foo bar\nHello World!');
});

test('FlutterError calls default error', () async {
var called = false;
final defaultError = (FlutterErrorDetails errorDetails) async {
Expand Down