blob: 23ed4ecea288580b2d3190923e11c45d75c2010f [file] [log] [blame]
// Copyright 2019, the Flutter project authors. Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.
part of firebase_crashlytics;
/// The entry point for accessing Crashlytics.
///
/// You can get an instance by calling `Crashlytics.instance`.
class Crashlytics {
static final Crashlytics instance = Crashlytics();
/// Set to true to have errors sent to Crashlytics while in debug mode. By
/// default this is false.
bool enableInDevMode = false;
/// Keys to be included with report.
final Map<String, dynamic> _keys = <String, dynamic>{};
/// Logs to be included with report.
final ListQueue<String> _logs = ListQueue<String>(15);
int _logSize = 0;
@visibleForTesting
static const MethodChannel channel =
MethodChannel('plugins.flutter.io/firebase_crashlytics');
/// Submits report of a non-fatal error caught by the Flutter framework.
/// to Firebase Crashlytics.
Future<void> recordFlutterError(FlutterErrorDetails details) async {
print('Flutter error caught by Crashlytics plugin:');
_recordError(details.exceptionAsString(), details.stack,
context: details.context,
information: details.informationCollector == null
? null
: details.informationCollector());
}
/// Submits a report of a non-fatal error.
///
/// For errors generated by the Flutter framework, use [recordFlutterError] instead.
Future<void> recordError(dynamic exception, StackTrace stack,
{dynamic context}) async {
print('Error caught by Crashlytics plugin <recordError>:');
_recordError(exception, stack, context: context);
}
void crash() {
throw StateError('Error thrown by Crashlytics plugin');
}
/// Reports the global value for debug mode.
/// TODO(kroikie): Clarify what this means in context of both Android and iOS.
Future<bool> isDebuggable() async {
final bool result =
await channel.invokeMethod<bool>('Crashlytics#isDebuggable');
return result;
}
/// Returns Crashlytics SDK version.
Future<String> getVersion() async {
final String result =
await channel.invokeMethod<String>('Crashlytics#getVersion');
return result;
}
/// Add text logging that will be sent with your next report. `msg` will be
/// printed to the console when in debug mode. Each report has a rolling max
/// of 64k of logs, older logs are removed to allow newer logs to fit within
/// the limit.
void log(String msg) {
_logSize += Uint8List.fromList(msg.codeUnits).length;
_logs.add(msg);
// Remove oldest log till logSize is no more than 64K.
while (_logSize > 65536) {
final String first = _logs.removeFirst();
_logSize -= Uint8List.fromList(first.codeUnits).length;
}
}
void _setValue(String key, dynamic value) {
// Check that only 64 keys are set.
if (_keys.containsKey(key) || _keys.length <= 64) {
_keys[key] = value;
}
}
/// Sets a value to be associated with a given key for your crash data.
void setBool(String key, bool value) {
_setValue(key, value);
}
/// Sets a value to be associated with a given key for your crash data.
void setDouble(String key, double value) {
_setValue(key, value);
}
/// Sets a value to be associated with a given key for your crash data.
void setInt(String key, int value) {
_setValue(key, value);
}
/// Sets a value to be associated with a given key for your crash data.
void setString(String key, String value) {
_setValue(key, value);
}
/// Optionally set a end-user's name or username for display within the
/// Crashlytics UI. Please be mindful of end-user's privacy.
Future<void> setUserEmail(String email) async {
await channel.invokeMethod<void>(
'Crashlytics#setUserEmail', <String, dynamic>{'email': email});
}
/// Specify a user identifier which will be visible in the Crashlytics UI.
/// Please be mindful of end-user's privacy.
Future<void> setUserIdentifier(String identifier) async {
await channel.invokeMethod<void>('Crashlytics#setUserIdentifier',
<String, dynamic>{'identifier': identifier});
}
/// Specify a user name which will be visible in the Crashlytics UI. Please
/// be mindful of end-user's privacy.
Future<void> setUserName(String name) async {
await channel.invokeMethod<void>(
'Crashlytics#setUserName', <String, dynamic>{'name': name});
}
List<Map<String, dynamic>> _prepareKeys() {
final List<Map<String, dynamic>> crashlyticsKeys = <Map<String, dynamic>>[];
for (String key in _keys.keys) {
final dynamic value = _keys[key];
final Map<String, dynamic> crashlyticsKey = <String, dynamic>{
'key': key,
'value': value
};
if (value is int) {
crashlyticsKey['type'] = 'int';
} else if (value is double) {
crashlyticsKey['type'] = 'double';
} else if (value is String) {
crashlyticsKey['type'] = 'string';
} else if (value is bool) {
crashlyticsKey['type'] = 'boolean';
}
crashlyticsKeys.add(crashlyticsKey);
}
return crashlyticsKeys;
}
@visibleForTesting
List<Map<String, String>> getStackTraceElements(List<String> lines) {
final List<Map<String, String>> elements = <Map<String, String>>[];
for (String line in lines) {
final List<String> lineParts = line.split(RegExp('\\s+'));
try {
final String fileName = lineParts[0];
final String lineNumber = lineParts[1].contains(":")
? lineParts[1].substring(0, lineParts[1].indexOf(":")).trim()
: lineParts[1];
final Map<String, String> element = <String, String>{
'file': fileName,
'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();
final String methodName =
lineParts[2].substring(lineParts[2].indexOf(".") + 1).trim();
element['class'] = className;
element['method'] = methodName;
} else {
element['method'] = lineParts[2];
}
elements.add(element);
} catch (e) {
print(e.toString());
}
}
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, 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) {
// 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 {
// 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);
// 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);
}
}
}