[firebase_crashlytics] Fix Printing, README, Changelog & log FlutterErrorDetails.informationCollector (#1912)

diff --git a/packages/firebase_crashlytics/CHANGELOG.md b/packages/firebase_crashlytics/CHANGELOG.md
index 17da299..5f4517d 100644
--- a/packages/firebase_crashlytics/CHANGELOG.md
+++ b/packages/firebase_crashlytics/CHANGELOG.md
@@ -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
diff --git a/packages/firebase_crashlytics/README.md b/packages/firebase_crashlytics/README.md
index 1876b3f..9f3980a 100644
--- a/packages/firebase_crashlytics/README.md
+++ b/packages/firebase_crashlytics/README.md
@@ -11,13 +11,14 @@
 ## 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()
@@ -29,7 +30,7 @@
 }
 ```
 
-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
@@ -41,14 +42,14 @@
 }
 ```
 
-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].
@@ -56,18 +57,18 @@
 ```
 
 *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
 
@@ -85,20 +86,32 @@
   // 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.
 ```
 
@@ -107,7 +120,7 @@
 ## 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
 
diff --git a/packages/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/firebasecrashlytics/FirebaseCrashlyticsPlugin.java b/packages/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/firebasecrashlytics/FirebaseCrashlyticsPlugin.java
index b3b04c6..0368738 100644
--- a/packages/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/firebasecrashlytics/FirebaseCrashlyticsPlugin.java
+++ b/packages/firebase_crashlytics/android/src/main/java/io/flutter/plugins/firebase/crashlytics/firebasecrashlytics/FirebaseCrashlyticsPlugin.java
@@ -70,6 +70,15 @@
       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")) {
diff --git a/packages/firebase_crashlytics/example/test_driver/crashlytics.dart b/packages/firebase_crashlytics/example/test_driver/crashlytics.dart
index 40bb087..2cd26d0 100644
--- a/packages/firebase_crashlytics/example/test_driver/crashlytics.dart
+++ b/packages/firebase_crashlytics/example/test_driver/crashlytics.dart
@@ -24,11 +24,13 @@
     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'),
+            ]));
   });
 }
diff --git a/packages/firebase_crashlytics/ios/Classes/FirebaseCrashlyticsPlugin.m b/packages/firebase_crashlytics/ios/Classes/FirebaseCrashlyticsPlugin.m
index f84a0ab..d135491 100644
--- a/packages/firebase_crashlytics/ios/Classes/FirebaseCrashlyticsPlugin.m
+++ b/packages/firebase_crashlytics/ios/Classes/FirebaseCrashlyticsPlugin.m
@@ -59,15 +59,28 @@
       }
     }
 
+    // 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]);
diff --git a/packages/firebase_crashlytics/lib/src/firebase_crashlytics.dart b/packages/firebase_crashlytics/lib/src/firebase_crashlytics.dart
index 1409c50..23ed4ec 100644
--- a/packages/firebase_crashlytics/lib/src/firebase_crashlytics.dart
+++ b/packages/firebase_crashlytics/lib/src/firebase_crashlytics.dart
@@ -30,7 +30,10 @@
     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.
@@ -165,6 +168,12 @@
           '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();
@@ -185,29 +194,65 @@
     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);
     }
   }
 }
diff --git a/packages/firebase_crashlytics/pubspec.yaml b/packages/firebase_crashlytics/pubspec.yaml
index d2d3d85..4b4c063 100644
--- a/packages/firebase_crashlytics/pubspec.yaml
+++ b/packages/firebase_crashlytics/pubspec.yaml
@@ -2,7 +2,7 @@
 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 <flutter-dev@google.com>
 homepage: https://github.com/flutter/plugins/tree/master/packages/firebase_crashlytics
 
diff --git a/packages/firebase_crashlytics/test/firebase_crashlytics_test.dart b/packages/firebase_crashlytics/test/firebase_crashlytics_test.dart
index 5fef585..da6daff 100644
--- a/packages/firebase_crashlytics/test/firebase_crashlytics_test.dart
+++ b/packages/firebase_crashlytics/test/firebase_crashlytics_test.dart
@@ -16,6 +16,8 @@
           .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':
@@ -38,6 +40,10 @@
         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;
@@ -50,6 +56,7 @@
       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');