Skip to content

Commit d593269

Browse files
authored
Implement error type identifier to mitigate obfuscated Flutter issue titles (#2170)
* try to mitigate runtime type not being obfuscated * fix imports * Remove prints * Update * Update * Update exception_type_identifier.dart * Add caching * Update * split up dart:io and dart:html exceptions * fix analyze * Update CHANGELOG * update * Add more tests * Update docs * Update options docs * remove print * remove CustomException * import with show * try fix test * Update CHANGELOG.md * Update CHANGELOG.md * Fix analyze * try fix test * Update CHANGELOG.md
1 parent ed7286c commit d593269

18 files changed

+508
-27
lines changed

CHANGELOG.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,33 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
### Improvements
6+
7+
- Add error type identifier to improve obfuscated Flutter issue titles ([#2170](https://github.com/getsentry/sentry-dart/pull/2170))
8+
- Example: transforms issue titles from `GA` to `FlutterError` or `minified:nE` to `FlutterError`
9+
- This is enabled automatically and will change grouping if you already have issues with obfuscated titles
10+
- If you want to disable this feature, set `enableExceptionTypeIdentification` to `false` in your Sentry options
11+
- You can add your custom exception identifier if there are exceptions that we do not identify out of the box
12+
```dart
13+
// How to add your own custom exception identifier
14+
class MyCustomExceptionIdentifier implements ExceptionIdentifier {
15+
@override
16+
String? identifyType(Exception exception) {
17+
if (exception is MyCustomException) {
18+
return 'MyCustomException';
19+
}
20+
if (exception is MyOtherCustomException) {
21+
return 'MyOtherCustomException';
22+
}
23+
return null;
24+
}
25+
}
26+
27+
SentryFlutter.init((options) =>
28+
options..prependExceptionTypeIdentifier(MyCustomExceptionIdentifier()));
29+
```
30+
331
## 8.5.0
432

533
### Features

dart/lib/sentry.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ export 'src/sentry_baggage.dart';
4040
export 'src/exception_cause_extractor.dart';
4141
export 'src/exception_cause.dart';
4242
export 'src/exception_stacktrace_extractor.dart';
43+
export 'src/exception_type_identifier.dart';
4344
// URL
4445
// ignore: invalid_export_of_internal_element
4546
export 'src/utils/http_sanitizer.dart';
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import 'package:http/http.dart' show ClientException;
2+
import 'dart:async' show TimeoutException, AsyncError, DeferredLoadException;
3+
import '../sentry.dart';
4+
5+
import 'dart_exception_type_identifier_io.dart'
6+
if (dart.library.html) 'dart_exception_type_identifier_web.dart';
7+
8+
class DartExceptionTypeIdentifier implements ExceptionTypeIdentifier {
9+
@override
10+
String? identifyType(dynamic throwable) {
11+
// dart:core
12+
if (throwable is ArgumentError) return 'ArgumentError';
13+
if (throwable is AssertionError) return 'AssertionError';
14+
if (throwable is ConcurrentModificationError) {
15+
return 'ConcurrentModificationError';
16+
}
17+
if (throwable is FormatException) return 'FormatException';
18+
if (throwable is IndexError) return 'IndexError';
19+
if (throwable is NoSuchMethodError) return 'NoSuchMethodError';
20+
if (throwable is OutOfMemoryError) return 'OutOfMemoryError';
21+
if (throwable is RangeError) return 'RangeError';
22+
if (throwable is StackOverflowError) return 'StackOverflowError';
23+
if (throwable is StateError) return 'StateError';
24+
if (throwable is TypeError) return 'TypeError';
25+
if (throwable is UnimplementedError) return 'UnimplementedError';
26+
if (throwable is UnsupportedError) return 'UnsupportedError';
27+
// not adding Exception or Error because it's too generic
28+
29+
// dart:async
30+
if (throwable is TimeoutException) return 'TimeoutException';
31+
if (throwable is AsyncError) return 'FutureTimeout';
32+
if (throwable is DeferredLoadException) return 'DeferredLoadException';
33+
// not adding ParallelWaitError because it's not supported in dart 2.17.0
34+
35+
// dart http package
36+
if (throwable is ClientException) return 'ClientException';
37+
38+
// platform specific exceptions
39+
return identifyPlatformSpecificException(throwable);
40+
}
41+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import 'dart:io';
2+
3+
import 'package:meta/meta.dart';
4+
5+
@internal
6+
String? identifyPlatformSpecificException(dynamic throwable) {
7+
if (throwable is FileSystemException) return 'FileSystemException';
8+
if (throwable is HttpException) return 'HttpException';
9+
if (throwable is SocketException) return 'SocketException';
10+
if (throwable is HandshakeException) return 'HandshakeException';
11+
if (throwable is CertificateException) return 'CertificateException';
12+
if (throwable is TlsException) return 'TlsException';
13+
return null;
14+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import 'package:meta/meta.dart';
2+
3+
@internal
4+
String? identifyPlatformSpecificException(dynamic throwable) {
5+
return null;
6+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import 'package:meta/meta.dart';
2+
3+
/// An abstract class for identifying the type of Dart errors and exceptions.
4+
///
5+
/// It's used in scenarios where error types need to be determined in obfuscated builds
6+
/// as [runtimeType] is not reliable in such cases.
7+
///
8+
/// Implement this class to create custom error type identifiers for errors or exceptions.
9+
/// that we do not support out of the box.
10+
///
11+
/// Example:
12+
/// ```dart
13+
/// class MyExceptionTypeIdentifier implements ExceptionTypeIdentifier {
14+
/// @override
15+
/// String? identifyType(dynamic throwable) {
16+
/// if (throwable is MyCustomError) return 'MyCustomError';
17+
/// return null;
18+
/// }
19+
/// }
20+
/// ```
21+
abstract class ExceptionTypeIdentifier {
22+
String? identifyType(dynamic throwable);
23+
}
24+
25+
extension CacheableExceptionIdentifier on ExceptionTypeIdentifier {
26+
ExceptionTypeIdentifier withCache() => CachingExceptionTypeIdentifier(this);
27+
}
28+
29+
@visibleForTesting
30+
class CachingExceptionTypeIdentifier implements ExceptionTypeIdentifier {
31+
@visibleForTesting
32+
ExceptionTypeIdentifier get identifier => _identifier;
33+
final ExceptionTypeIdentifier _identifier;
34+
35+
final Map<Type, String?> _knownExceptionTypes = {};
36+
37+
CachingExceptionTypeIdentifier(this._identifier);
38+
39+
@override
40+
String? identifyType(dynamic throwable) {
41+
final runtimeType = throwable.runtimeType;
42+
if (_knownExceptionTypes.containsKey(runtimeType)) {
43+
return _knownExceptionTypes[runtimeType];
44+
}
45+
46+
final identifiedType = _identifier.identifyType(throwable);
47+
48+
if (identifiedType != null) {
49+
_knownExceptionTypes[runtimeType] = identifiedType;
50+
}
51+
52+
return identifiedType;
53+
}
54+
}

dart/lib/src/sentry.dart

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'dart:async';
22

33
import 'package:meta/meta.dart';
44

5+
import 'dart_exception_type_identifier.dart';
56
import 'metrics/metrics_api.dart';
67
import 'run_zoned_guarded_integration.dart';
78
import 'event_processor/enricher/enricher_event_processor.dart';
@@ -85,6 +86,8 @@ class Sentry {
8586
options.addEventProcessor(EnricherEventProcessor(options));
8687
options.addEventProcessor(ExceptionEventProcessor(options));
8788
options.addEventProcessor(DeduplicationEventProcessor(options));
89+
90+
options.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier());
8891
}
8992

9093
/// This method reads available environment variables and uses them

dart/lib/src/sentry_client.dart

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
import 'dart:async';
22
import 'dart:math';
3+
34
import 'package:meta/meta.dart';
4-
import 'utils/stacktrace_utils.dart';
5-
import 'metrics/metric.dart';
6-
import 'metrics/metrics_aggregator.dart';
7-
import 'sentry_baggage.dart';
8-
import 'sentry_attachment/sentry_attachment.dart';
95

6+
import 'client_reports/client_report_recorder.dart';
7+
import 'client_reports/discard_reason.dart';
108
import 'event_processor.dart';
119
import 'hint.dart';
12-
import 'sentry_trace_context_header.dart';
13-
import 'sentry_user_feedback.dart';
14-
import 'transport/rate_limiter.dart';
10+
import 'metrics/metric.dart';
11+
import 'metrics/metrics_aggregator.dart';
1512
import 'protocol.dart';
1613
import 'scope.dart';
14+
import 'sentry_attachment/sentry_attachment.dart';
15+
import 'sentry_baggage.dart';
16+
import 'sentry_envelope.dart';
1717
import 'sentry_exception_factory.dart';
1818
import 'sentry_options.dart';
1919
import 'sentry_stack_trace_factory.dart';
20+
import 'sentry_trace_context_header.dart';
21+
import 'sentry_user_feedback.dart';
22+
import 'transport/data_category.dart';
2023
import 'transport/http_transport.dart';
2124
import 'transport/noop_transport.dart';
25+
import 'transport/rate_limiter.dart';
2226
import 'transport/spotlight_http_transport.dart';
2327
import 'transport/task_queue.dart';
2428
import 'utils/isolate_utils.dart';
29+
import 'utils/stacktrace_utils.dart';
2530
import 'version.dart';
26-
import 'sentry_envelope.dart';
27-
import 'client_reports/client_report_recorder.dart';
28-
import 'client_reports/discard_reason.dart';
29-
import 'transport/data_category.dart';
3031

3132
/// Default value for [SentryUser.ipAddress]. It gets set when an event does not have
3233
/// a user and IP address. Only applies if [SentryOptions.sendDefaultPii] is set

dart/lib/src/sentry_exception_factory.dart

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,9 @@
1-
import 'utils/stacktrace_utils.dart';
2-
3-
import 'recursive_exception_cause_extractor.dart';
41
import 'protocol.dart';
2+
import 'recursive_exception_cause_extractor.dart';
53
import 'sentry_options.dart';
64
import 'sentry_stack_trace_factory.dart';
75
import 'throwable_mechanism.dart';
6+
import 'utils/stacktrace_utils.dart';
87

98
/// class to convert Dart Error and exception to SentryException
109
class SentryExceptionFactory {
@@ -62,10 +61,22 @@ class SentryExceptionFactory {
6261
final stackTraceString = stackTrace.toString();
6362
final value = throwableString.replaceAll(stackTraceString, '').trim();
6463

64+
String errorTypeName = throwable.runtimeType.toString();
65+
66+
if (_options.enableExceptionTypeIdentification) {
67+
for (final errorTypeIdentifier in _options.exceptionTypeIdentifiers) {
68+
final identifiedErrorType = errorTypeIdentifier.identifyType(throwable);
69+
if (identifiedErrorType != null) {
70+
errorTypeName = identifiedErrorType;
71+
break;
72+
}
73+
}
74+
}
75+
6576
// if --obfuscate feature is enabled, 'type' won't be human readable.
6677
// https://flutter.dev/docs/deployment/obfuscate#caveat
6778
return SentryException(
68-
type: (throwable.runtimeType).toString(),
79+
type: errorTypeName,
6980
value: value.isNotEmpty ? value : null,
7081
mechanism: mechanism,
7182
stackTrace: sentryStackTrace,

dart/lib/src/sentry_options.dart

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
import 'dart:async';
22
import 'dart:developer';
33

4-
import 'package:meta/meta.dart';
54
import 'package:http/http.dart';
5+
import 'package:meta/meta.dart';
66

77
import '../sentry.dart';
88
import 'client_reports/client_report_recorder.dart';
99
import 'client_reports/noop_client_report_recorder.dart';
10-
import 'sentry_exception_factory.dart';
11-
import 'sentry_stack_trace_factory.dart';
1210
import 'diagnostic_logger.dart';
1311
import 'environment/environment_variables.dart';
1412
import 'noop_client.dart';
13+
import 'sentry_exception_factory.dart';
14+
import 'sentry_stack_trace_factory.dart';
1515
import 'transport/noop_transport.dart';
1616
import 'version.dart';
1717

@@ -452,6 +452,33 @@ class SentryOptions {
452452
/// Settings this to `false` will set the `level` to [SentryLevel.error].
453453
bool markAutomaticallyCollectedErrorsAsFatal = true;
454454

455+
/// Enables identification of exception types in obfuscated builds.
456+
/// When true, the SDK will attempt to identify common exception types
457+
/// to improve readability of obfuscated issue titles.
458+
///
459+
/// If you already have events with obfuscated issue titles this will change grouping.
460+
///
461+
/// Default: `true`
462+
bool enableExceptionTypeIdentification = true;
463+
464+
final List<ExceptionTypeIdentifier> _exceptionTypeIdentifiers = [];
465+
466+
List<ExceptionTypeIdentifier> get exceptionTypeIdentifiers =>
467+
List.unmodifiable(_exceptionTypeIdentifiers);
468+
469+
void addExceptionTypeIdentifierByIndex(
470+
int index, ExceptionTypeIdentifier exceptionTypeIdentifier) {
471+
_exceptionTypeIdentifiers.insert(
472+
index, exceptionTypeIdentifier.withCache());
473+
}
474+
475+
/// Adds an exception type identifier to the beginning of the list.
476+
/// This ensures it is processed first and takes precedence over existing identifiers.
477+
void prependExceptionTypeIdentifier(
478+
ExceptionTypeIdentifier exceptionTypeIdentifier) {
479+
addExceptionTypeIdentifierByIndex(0, exceptionTypeIdentifier);
480+
}
481+
455482
/// The Spotlight configuration.
456483
/// Disabled by default.
457484
/// ```dart

0 commit comments

Comments
 (0)