Unverified Commit 602475e5 authored by Jenn Magder's avatar Jenn Magder Committed by GitHub

Sanitize error message sent to GitHub crash reporter (#53936)

parent 61b33617
......@@ -174,8 +174,7 @@ Future<void> _informUserOfCrash(List<String> args, dynamic error, StackTrace sta
final String command = _crashCommand(args);
final String gitHubTemplateURL = await gitHubTemplateCreator.toolCrashIssueTemplateGitHubURL(
command,
errorString,
_crashException(error),
error,
stackTrace,
doctorText
);
......
......@@ -5,15 +5,20 @@
import 'dart:async';
import 'package:file/file.dart';
import 'package:intl/intl.dart';
import 'package:meta/meta.dart';
import '../base/file_system.dart';
import '../base/io.dart';
import '../base/logger.dart';
import '../base/process.dart';
import '../build_system/exceptions.dart';
import '../convert.dart';
import '../devfs.dart';
import '../flutter_manifest.dart';
import '../flutter_project_metadata.dart';
import '../project.dart';
import '../version.dart';
/// Provide suggested GitHub issue templates to user when Flutter encounters an error.
class GitHubTemplateCreator {
......@@ -36,16 +41,51 @@ class GitHubTemplateCreator {
return 'https://github.com/flutter/flutter/issues?q=is%3Aissue+${Uri.encodeQueryComponent(errorString)}';
}
/// Restricts exception object strings to contain only information about tool internals.
static String sanitizedCrashException(dynamic error) {
if (error is ProcessException) {
// Suppress args.
return 'ProcessException: ${error.message} Command: ${error.executable}, OS error code: ${error.errorCode}';
} else if (error is FileSystemException) {
// Suppress path.
return 'FileSystemException: ${error.message}, ${error.osError}';
} else if (error is SocketException) {
// Suppress address and port.
return 'SocketException: ${error.message}, ${error.osError}';
} else if (error is DevFSException) {
// Suppress underlying error.
return 'DevFSException: ${error.message}';
} else if (error is NoSuchMethodError
|| error is ArgumentError
|| error is VersionCheckError
|| error is MissingDefineException
|| error is UnsupportedError
|| error is UnimplementedError
|| error is StateError
|| error is ProcessExit
|| error is OSError) {
// These exception objects only reference tool internals, print the whole error.
return '${error.runtimeType}: $error';
} else if (error is Error) {
return '${error.runtimeType}: ${LineSplitter.split(error.stackTrace.toString()).take(1)}';
} else if (error is String) {
// Force comma separator to standardize.
return 'String: <${NumberFormat(null, 'en_US').format(error.length)} characters>';
}
// Exception, other.
return error.runtimeType.toString();
}
/// GitHub URL to present to the user containing encoded suggested template.
///
/// Shorten the URL, if possible.
Future<String> toolCrashIssueTemplateGitHubURL(
String command,
String errorString,
String exception,
dynamic error,
StackTrace stackTrace,
String doctorText
) async {
final String errorString = sanitizedCrashException(error);
final String title = '[tool_crash] $errorString';
final String body = '''
## Command
......@@ -59,9 +99,9 @@ $command
3. ...
## Logs
$exception
$errorString
```
${LineSplitter.split(stackTrace.toString()).take(20).join('\n')}
${LineSplitter.split(stackTrace.toString()).take(25).join('\n')}
```
```
$doctorText
......
......@@ -6,6 +6,7 @@ import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_tools/src/base/io.dart';
import 'package:flutter_tools/src/base/logger.dart';
import 'package:flutter_tools/src/devfs.dart';
import 'package:flutter_tools/src/project.dart';
import 'package:flutter_tools/src/reporting/github_template.dart';
......@@ -31,15 +32,127 @@ void main() {
);
});
group('sanitized error message', () {
testWithoutContext('ProcessException', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
const ProcessException('cd', <String>['path/to/something'])
),
'ProcessException: Command: cd, OS error code: 0',
);
expect(
GitHubTemplateCreator.sanitizedCrashException(
const ProcessException('cd', <String>['path/to/something'], 'message')
),
'ProcessException: message Command: cd, OS error code: 0',
);
expect(
GitHubTemplateCreator.sanitizedCrashException(
const ProcessException('cd', <String>['path/to/something'], 'message', -19)
),
'ProcessException: message Command: cd, OS error code: -19',
);
});
testWithoutContext('FileSystemException', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
const FileSystemException('delete failed', 'path/to/something')
),
'FileSystemException: delete failed, null',
);
expect(
GitHubTemplateCreator.sanitizedCrashException(
const FileSystemException('delete failed', 'path/to/something', OSError('message', -19))
),
'FileSystemException: delete failed, OS Error: message, errno = -19',
);
});
testWithoutContext('SocketException', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
SocketException(
'message',
osError: const OSError('message', -19),
address: InternetAddress.anyIPv6,
port: 2000
)
),
'SocketException: message, OS Error: message, errno = -19',
);
});
testWithoutContext('DevFSException', () {
final StackTrace stackTrace = StackTrace.fromString('''
#0 _File.open.<anonymous closure> (dart:io/file_impl.dart:366:9)
#1 _rootRunUnary (dart:async/zone.dart:1141:38)''');
expect(
GitHubTemplateCreator.sanitizedCrashException(
DevFSException('message', ArgumentError('argument error message'), stackTrace)
),
'DevFSException: message',
);
});
testWithoutContext('ArgumentError', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
ArgumentError('argument error message')
),
'ArgumentError: Invalid argument(s): argument error message',
);
});
testWithoutContext('Error', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
FakeError()
),
'FakeError: (#0 _File.open.<anonymous closure> (dart:io/file_impl.dart:366:9))',
);
});
testWithoutContext('String', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
'May have non-tool-internal info, very long string, 0b8abb4724aa590dd0f429683339b'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
'24aa590dd0f429683339b1e045a1594d0b8abb4724aa590dd0f429683339b1e045a1594d0b8abb'
),
'String: <1,016 characters>',
);
});
testWithoutContext('Exception', () {
expect(
GitHubTemplateCreator.sanitizedCrashException(
Exception('May have non-tool-internal info')
),
'_Exception',
);
});
});
group('new issue template URL', () {
StackTrace stackTrace;
Error error;
const String command = 'flutter test';
const String errorString = 'this is a 100% error';
const String exception = 'failing to succeed!!!';
const String doctorText = ' [✓] Flutter (Channel report';
setUp(() async {
stackTrace = StackTrace.fromString('trace');
error = ArgumentError('argument error message');
});
testUsingContext('shortened', () async {
......@@ -50,7 +163,7 @@ void main() {
flutterProjectFactory: FlutterProjectFactory(),
);
expect(
await creator.toolCrashIssueTemplateGitHubURL(command, errorString, exception, stackTrace, doctorText),
await creator.toolCrashIssueTemplateGitHubURL(command, error, stackTrace, doctorText),
_kShortURL
);
}, overrides: <Type, Generator>{
......@@ -66,12 +179,14 @@ void main() {
flutterProjectFactory: FlutterProjectFactory(),
);
expect(
await creator.toolCrashIssueTemplateGitHubURL(command, errorString, exception, stackTrace, doctorText),
'https://github.com/flutter/flutter/issues/new?title=%5Btool_crash%5D+this+is+a+100%25+error&body=%23%'
'23+Command%0A%60%60%60%0Aflutter+test%0A%60%60%60%0A%0A%23%23+Steps+to+Reproduce%0A1.+...'
'%0A2.+...%0A3.+...%0A%0A%23%23+Logs%0Afailing+to+succeed%21%21%21%0A%60%60%60%0Atrace%0A'
'%60%60%60%0A%60%60%60%0A+%5B%E2%9C%93%5D+Flutter+%28Channel+report%0A%60%60%60%0A%0A%23%23'
'+Flutter+Application+Metadata%0ANo+pubspec+in+working+directory.%0A&labels=tool%2Csevere%3A+crash'
await creator.toolCrashIssueTemplateGitHubURL(command, error, stackTrace, doctorText),
'https://github.com/flutter/flutter/issues/new?title=%5Btool_crash%5D+ArgumentError%3A+'
'Invalid+argument%28s%29%3A+argument+error+message&body=%23%23+Command%0A%60%60%60%0A'
'flutter+test%0A%60%60%60%0A%0A%23%23+Steps+to+Reproduce%0A1.+...%0A2.+...%0A3.+...%0'
'A%0A%23%23+Logs%0AArgumentError%3A+Invalid+argument%28s%29%3A+argument+error+message'
'%0A%60%60%60%0Atrace%0A%60%60%60%0A%60%60%60%0A+%5B%E2%9C%93%5D+Flutter+%28Channel+r'
'eport%0A%60%60%60%0A%0A%23%23+Flutter+Application+Metadata%0ANo+pubspec+in+working+d'
'irectory.%0A&labels=tool%2Csevere%3A+crash'
);
expect(logger.traceText, contains('Failed to shorten GitHub template URL'));
}, overrides: <Type, Generator>{
......@@ -118,7 +233,7 @@ version:
project_type: app
''');
final String actualURL = await creator.toolCrashIssueTemplateGitHubURL(command, errorString, exception, stackTrace, doctorText);
final String actualURL = await creator.toolCrashIssueTemplateGitHubURL(command, error, stackTrace, doctorText);
final String actualBody = Uri.parse(actualURL).queryParameters['body'];
const String expectedBody = '''
## Command
......@@ -132,7 +247,7 @@ flutter test
3. ...
## Logs
failing to succeed!!!
ArgumentError: Invalid argument(s): argument error message
```
trace
```
......@@ -195,3 +310,10 @@ class SuccessShortenURLFakeHttpClient extends FakeHttpClient {
return SuccessFakeHttpClientRequest();
}
}
class FakeError implements Error {
@override
StackTrace get stackTrace => StackTrace.fromString('''
#0 _File.open.<anonymous closure> (dart:io/file_impl.dart:366:9)
#1 _rootRunUnary (dart:async/zone.dart:1141:38)''');
}
......@@ -82,7 +82,7 @@ void main() {
testUsingContext('GitHub issue template', () async {
const String templateURL = 'https://example.com/2';
when(mockGitHubTemplateCreator.toolCrashIssueTemplateGitHubURL(any, any, any, any, any))
when(mockGitHubTemplateCreator.toolCrashIssueTemplateGitHubURL(any, any, any, any))
.thenAnswer((_) async => templateURL);
final Completer<void> completer = Completer<void>();
// runner.run() asynchronously calls the exit function set above, so we
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment