Skip to content

Commit

Permalink
[firebase_crashlytics] Fix Printing, README, Changelog & log FlutterE…
Browse files Browse the repository at this point in the history
…rrorDetails.informationCollector (firebase#1912)
  • Loading branch information
creativecreatorormaybenot authored and collinjackson committed Jul 26, 2019
1 parent 8553cb7 commit c4d2bcd
Show file tree
Hide file tree
Showing 8 changed files with 135 additions and 35 deletions.
13 changes: 12 additions & 1 deletion packages/firebase_crashlytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
## 0.1.0+1

* Added additional exception information from the Flutter framework to the reports.
* Refactored debug printing of exceptions to be human-readable.
* Passing `null` stack traces is now supported.
* Added the "Error reported to Crashlytics." print statement that was previously missing.
* Updated `README.md` to include both the breaking change from `0.1.0` and the newly added
`recordError` function in the setup section.
* Adjusted `README.md` formatting.
* Fixed `recordFlutterError` method name in the `0.1.0` changelog entry.

## 0.1.0

* **Breaking Change** Renamed `onError` to `reportFlutterError`.
* **Breaking Change** Renamed `onError` to `recordFlutterError`.
* Added `recordError` method for errors caught using `runZoned`'s `onError`.

## 0.0.4+12
Expand Down
53 changes: 33 additions & 20 deletions packages/firebase_crashlytics/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ For Flutter plugins for other Firebase products, see [FlutterFire.md](https://gi
## Usage

### Import the firebase_crashlytics plugin
To use the firebase_crashlytics plugin, follow the [plugin installation instructions](https://pub.dartlang.org/packages/firebase_crashlytics#pub-pkg-tab-installing).

To use the `firebase_crashlytics` plugin, follow the [plugin installation instructions](https://pub.dartlang.org/packages/firebase_crashlytics#pub-pkg-tab-installing).

### Android integration

Enable the Google services by configuring the Gradle scripts as such.
Enable the Google services by configuring the Gradle scripts as such:

1. Add Fabric repository to the `[project]/android/build.gradle` file.
1. Add the Fabric repository to the `[project]/android/build.gradle` file.
```
repositories {
google()
Expand All @@ -29,7 +30,7 @@ repositories {
}
```

2. Add the classpaths to the `[project]/android/build.gradle` file.
2. Add the following classpaths to the `[project]/android/build.gradle` file.
```gradle
dependencies {
// Example existing classpath
Expand All @@ -41,33 +42,33 @@ dependencies {
}
```

2. Add the apply plugins to the `[project]/android/app/build.gradle` file.
2. Apply the following plugins in the `[project]/android/app/build.gradle` file.
```gradle
// ADD THIS AT THE BOTTOM
apply plugin: 'io.fabric'
apply plugin: 'com.google.gms.google-services'
```

*Note:* If this section is not completed you will get an error like this:
*Note:* If this section is not completed, you will get an error like this:
```
java.lang.IllegalStateException:
Default FirebaseApp is not initialized in this process [package name].
Make sure to call FirebaseApp.initializeApp(Context) first.
```

*Note:* When you are debugging on Android, use a device or AVD with Google Play services.
Otherwise you will not be able to use Firebase Crashlytics.
Otherwise, you will not be able to use Firebase Crashlytics.

### iOS Integration

Add the Crashlytics run scripts
Add the Crashlytics run scripts:

1. From Xcode select Runner from the project navigation.
1. Select the Build Phases tab.
1. Click + Add a new build phase, and select New Run Script Phase.
1. From Xcode select `Runner` from the project navigation.
1. Select the `Build Phases` tab.
1. Click `+ Add a new build phase`, and select `New Run Script Phase`.
1. Add `${PODS_ROOT}/Fabric/run` to the `Type a script...` text box.
1. If on Xcode 10 Add your app's built Info.plist location to the Build Phase's Input Files field.
Eg: `$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)`
1. If you are using Xcode 10, add the location of `Info.plist`, built by your app, to the `Build Phase's Input Files` field.
E.g.: `$(BUILT_PRODUCTS_DIR)/$(INFOPLIST_PATH)`

### Use the plugin

Expand All @@ -85,20 +86,32 @@ void main() {
// development.
Crashlytics.instance.enableInDevMode = true;
// Pass all uncaught errors to Crashlytics.
FlutterError.onError = (FlutterErrorDetails details) {
Crashlytics.instance.onError(details);
};
// Pass all uncaught errors from the framework to Crashlytics.
FlutterError.onError = Crashlytics.instance.recordFlutterError;
runApp(MyApp());
}
```

Overriding `FlutterError.onError` with `Crashlytics.instance.recordFlutterError` will automatically catch all
errors that are thrown from within the Flutter framework.
If you want to catch errors that occur in `runZoned`,
you can supply `Crashlytics.instance.recordError` to the `onError` parameter:
```dart
runZoned<Future<void>>(() async {
// ...
}, onError: Crashlytics.instance.recordError);
```

## Result

If an error is caught, you should see the following messages in your logs:
```
flutter: Error caught by Crashlytics plugin:
...
flutter: Flutter error caught by Crashlytics plugin:
// OR if you use recordError for runZoned:
flutter: Error caught by Crashlytics plugin <recordError>:
// Exception, context, information, and stack trace in debug mode
// OR if not in debug mode:
flutter: Error reported to Crashlytics.
```

Expand All @@ -107,7 +120,7 @@ flutter: Error reported to Crashlytics.
## Example

See the [example application](https://github.com/flutter/plugins/tree/master/packages/firebase_crashlytics/example) source
for a complete sample app using the Firebase Crashlytics.
for a complete sample app using `firebase_crashlytics`.

## Issues and feedback

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ public void onMethodCall(MethodCall call, Result result) {
exception.setStackTrace(elements.toArray(new StackTraceElement[elements.size()]));

Crashlytics.setString("exception", (String) call.argument("exception"));

// Set a "reason" (to match iOS) to show where the exception was thrown.
final String context = call.argument("context");
if (context != null) Crashlytics.setString("reason", "thrown " + context);

// Log information.
final String information = call.argument("information");
if (information != null && !information.isEmpty()) Crashlytics.log(information);

Crashlytics.logException(exception);
result.success("Error reported to Crashlytics.");
} else if (call.method.equals("Crashlytics#isDebuggable")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,13 @@ void main() {
crashlytics.setDouble('testDouble', 42.0);
crashlytics.setString('testString', 'bar');
Crashlytics.instance.log('testing');
await crashlytics.recordFlutterError(
FlutterErrorDetails(
await crashlytics.recordFlutterError(FlutterErrorDetails(
exception: 'testing',
stack: StackTrace.fromString(''),
),
);
context: DiagnosticsNode.message('during testing'),
informationCollector: () => <DiagnosticsNode>[
DiagnosticsNode.message('testing'),
DiagnosticsNode.message('information'),
]));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,15 +59,28 @@ - (void)handleMethodCall:(FlutterMethodCall *)call result:(FlutterResult)result
}
}

// Add additional information from the Flutter framework to the exception reported in
// Crashlytics. Using CLSLog instead of CLS_LOG to try to avoid the automatic inclusion of the
// line number. It also ensures that the log is only written to Crashlytics and not also to the
// offline log as explained here:
// https://support.crashlytics.com/knowledgebase/articles/92519-how-do-i-use-logging
// Although, that would only happen in debug mode, which this method call is never called in.
NSString *information = call.arguments[@"information"];
if ([information length] != 0) {
CLSLog(information);
}

// Report crash.
NSArray *errorElements = call.arguments[@"stackTraceElements"];
NSMutableArray *frames = [NSMutableArray array];
for (NSDictionary *errorElement in errorElements) {
[frames addObject:[self generateFrame:errorElement]];
}
[[Crashlytics sharedInstance] recordCustomExceptionName:call.arguments[@"exception"]
reason:call.arguments[@"context"]
frameArray:frames];
[[Crashlytics sharedInstance]
recordCustomExceptionName:call.arguments[@"exception"]
reason:[NSString
stringWithFormat:@"thrown %s", call.arguments[@"context"]]
frameArray:frames];
result(@"Error reported to Crashlytics.");
} else if ([@"Crashlytics#isDebuggable" isEqualToString:call.method]) {
result([NSNumber numberWithBool:[Crashlytics sharedInstance].debugMode]);
Expand Down
57 changes: 51 additions & 6 deletions packages/firebase_crashlytics/lib/src/firebase_crashlytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ class Crashlytics {
print('Flutter error caught by Crashlytics plugin:');

_recordError(details.exceptionAsString(), details.stack,
context: details.context);
context: details.context,
information: details.informationCollector == null
? null
: details.informationCollector());
}

/// Submits a report of a non-fatal error.
Expand Down Expand Up @@ -165,6 +168,12 @@ class Crashlytics {
'line': lineNumber,
};

// The next section would throw an exception in some cases if there was no stop here.
if (lineParts.length < 3) {
elements.add(element);
continue;
}

if (lineParts[2].contains(".")) {
final String className =
lineParts[2].substring(0, lineParts[2].indexOf(".")).trim();
Expand All @@ -185,29 +194,65 @@ class Crashlytics {
return elements;
}

// On top of the default exception components, [information] can be passed as well.
// This allows the developer to get a better understanding of exceptions thrown
// by the Flutter framework. [FlutterErrorDetails] often explain why an exception
// occurred and give useful background information in [FlutterErrorDetails.informationCollector].
// Crashlytics will log this information in addition to the stack trace.
// If [information] is `null` or empty, it will be ignored.
Future<void> _recordError(dynamic exception, StackTrace stack,
{dynamic context}) async {
{dynamic context, Iterable<DiagnosticsNode> information}) async {
bool inDebugMode = false;
if (!enableInDevMode) {
assert(inDebugMode = true);
}

final String _information = (information == null || information.isEmpty)
? ''
: (StringBuffer()..writeAll(information, '\n')).toString();

if (inDebugMode && !enableInDevMode) {
print(Trace.format(stack));
// If available, give context to the exception.
if (context != null)
print('The following exception was thrown $context:');

// Need to print the exception to explain why the exception was thrown.
print(exception);

// Print information provided by the Flutter framework about the exception.
if (_information.isNotEmpty) print('\n$_information');

// Not using Trace.format here to stick to the default stack trace format
// that Flutter developers are used to seeing.
if (stack != null) print('\n$stack');
} else {
// Report error
// The stack trace can be null. To avoid the following exception:
// Invalid argument(s): Cannot create a Trace from null.
// To avoid that exception, we can check for null and provide an empty stack trace.
stack ??= StackTrace.fromString('');

// Report error.
final List<String> stackTraceLines =
Trace.format(stack).trimRight().split('\n');
final List<Map<String, String>> stackTraceElements =
getStackTraceElements(stackTraceLines);
await channel
.invokeMethod<dynamic>('Crashlytics#onError', <String, dynamic>{

// The context is a string that "should be in a form that will make sense in
// English when following the word 'thrown'" according to the documentation for
// [FlutterErrorDetails.context]. It is displayed to the user on Crashlytics
// as the "reason", which is forced by iOS, with the "thrown" prefix added.
final String result = await channel
.invokeMethod<String>('Crashlytics#onError', <String, dynamic>{
'exception': "${exception.toString()}",
'context': '$context',
'information': _information,
'stackTraceElements': stackTraceElements,
'logs': _logs.toList(),
'keys': _prepareKeys(),
});

// Print result.
print(result);
}
}
}
2 changes: 1 addition & 1 deletion packages/firebase_crashlytics/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: firebase_crashlytics
description:
Flutter plugin for Firebase Crashlytics. It reports uncaught errors to the
Firebase console.
version: 0.1.0
version: 0.1.0+1
author: Flutter Team <[email protected]>
homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_crashlytics

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ void main() {
.setMockMethodCallHandler((MethodCall methodCall) async {
log.add(methodCall);
switch (methodCall.method) {
case 'Crashlytics#onError':
return 'Error reported to Crashlytics.';
case 'Crashlytics#isDebuggable':
return true;
case 'Crashlytics#setUserEmail':
Expand All @@ -38,6 +40,10 @@ void main() {
exception: 'foo exception',
stack: StackTrace.current,
library: 'foo library',
informationCollector: () => <DiagnosticsNode>[
DiagnosticsNode.message('test message'),
DiagnosticsNode.message('second message'),
],
context: ErrorDescription('foo context'),
);
crashlytics.enableInDevMode = true;
Expand All @@ -50,6 +56,7 @@ void main() {
expect(log[0].method, 'Crashlytics#onError');
expect(log[0].arguments['exception'], 'foo exception');
expect(log[0].arguments['context'], 'foo context');
expect(log[0].arguments['information'], 'test message\nsecond message');
expect(log[0].arguments['logs'], isNotEmpty);
expect(log[0].arguments['logs'], contains('foo'));
expect(log[0].arguments['keys'][0]['key'], 'testBool');
Expand Down

0 comments on commit c4d2bcd

Please sign in to comment.