Skip to content

Commit

Permalink
Implement error type identifier to mitigate obfuscated Flutter issue …
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
buenaflor authored Jul 25, 2024
1 parent ed7286c commit d593269
Show file tree
Hide file tree
Showing 18 changed files with 508 additions and 27 deletions.
28 changes: 28 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,33 @@
# Changelog

## Unreleased

### Improvements

- Add error type identifier to improve obfuscated Flutter issue titles ([#2170](https://github.com/getsentry/sentry-dart/pull/2170))
- Example: transforms issue titles from `GA` to `FlutterError` or `minified:nE` to `FlutterError`
- This is enabled automatically and will change grouping if you already have issues with obfuscated titles
- If you want to disable this feature, set `enableExceptionTypeIdentification` to `false` in your Sentry options
- You can add your custom exception identifier if there are exceptions that we do not identify out of the box
```dart
// How to add your own custom exception identifier
class MyCustomExceptionIdentifier implements ExceptionIdentifier {
@override
String? identifyType(Exception exception) {
if (exception is MyCustomException) {
return 'MyCustomException';
}
if (exception is MyOtherCustomException) {
return 'MyOtherCustomException';
}
return null;
}
}
SentryFlutter.init((options) =>
options..prependExceptionTypeIdentifier(MyCustomExceptionIdentifier()));
```

## 8.5.0

### Features
Expand Down
1 change: 1 addition & 0 deletions dart/lib/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export 'src/sentry_baggage.dart';
export 'src/exception_cause_extractor.dart';
export 'src/exception_cause.dart';
export 'src/exception_stacktrace_extractor.dart';
export 'src/exception_type_identifier.dart';
// URL
// ignore: invalid_export_of_internal_element
export 'src/utils/http_sanitizer.dart';
Expand Down
41 changes: 41 additions & 0 deletions dart/lib/src/dart_exception_type_identifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import 'package:http/http.dart' show ClientException;
import 'dart:async' show TimeoutException, AsyncError, DeferredLoadException;
import '../sentry.dart';

import 'dart_exception_type_identifier_io.dart'
if (dart.library.html) 'dart_exception_type_identifier_web.dart';

class DartExceptionTypeIdentifier implements ExceptionTypeIdentifier {
@override
String? identifyType(dynamic throwable) {
// dart:core
if (throwable is ArgumentError) return 'ArgumentError';
if (throwable is AssertionError) return 'AssertionError';
if (throwable is ConcurrentModificationError) {
return 'ConcurrentModificationError';
}
if (throwable is FormatException) return 'FormatException';
if (throwable is IndexError) return 'IndexError';
if (throwable is NoSuchMethodError) return 'NoSuchMethodError';
if (throwable is OutOfMemoryError) return 'OutOfMemoryError';
if (throwable is RangeError) return 'RangeError';
if (throwable is StackOverflowError) return 'StackOverflowError';
if (throwable is StateError) return 'StateError';
if (throwable is TypeError) return 'TypeError';
if (throwable is UnimplementedError) return 'UnimplementedError';
if (throwable is UnsupportedError) return 'UnsupportedError';
// not adding Exception or Error because it's too generic

// dart:async
if (throwable is TimeoutException) return 'TimeoutException';
if (throwable is AsyncError) return 'FutureTimeout';
if (throwable is DeferredLoadException) return 'DeferredLoadException';
// not adding ParallelWaitError because it's not supported in dart 2.17.0

// dart http package
if (throwable is ClientException) return 'ClientException';

// platform specific exceptions
return identifyPlatformSpecificException(throwable);
}
}
14 changes: 14 additions & 0 deletions dart/lib/src/dart_exception_type_identifier_io.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import 'dart:io';

import 'package:meta/meta.dart';

@internal
String? identifyPlatformSpecificException(dynamic throwable) {
if (throwable is FileSystemException) return 'FileSystemException';
if (throwable is HttpException) return 'HttpException';
if (throwable is SocketException) return 'SocketException';
if (throwable is HandshakeException) return 'HandshakeException';
if (throwable is CertificateException) return 'CertificateException';
if (throwable is TlsException) return 'TlsException';
return null;
}
6 changes: 6 additions & 0 deletions dart/lib/src/dart_exception_type_identifier_web.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import 'package:meta/meta.dart';

@internal
String? identifyPlatformSpecificException(dynamic throwable) {
return null;
}
54 changes: 54 additions & 0 deletions dart/lib/src/exception_type_identifier.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import 'package:meta/meta.dart';

/// An abstract class for identifying the type of Dart errors and exceptions.
///
/// It's used in scenarios where error types need to be determined in obfuscated builds
/// as [runtimeType] is not reliable in such cases.
///
/// Implement this class to create custom error type identifiers for errors or exceptions.
/// that we do not support out of the box.
///
/// Example:
/// ```dart
/// class MyExceptionTypeIdentifier implements ExceptionTypeIdentifier {
/// @override
/// String? identifyType(dynamic throwable) {
/// if (throwable is MyCustomError) return 'MyCustomError';
/// return null;
/// }
/// }
/// ```
abstract class ExceptionTypeIdentifier {
String? identifyType(dynamic throwable);
}

extension CacheableExceptionIdentifier on ExceptionTypeIdentifier {
ExceptionTypeIdentifier withCache() => CachingExceptionTypeIdentifier(this);
}

@visibleForTesting
class CachingExceptionTypeIdentifier implements ExceptionTypeIdentifier {
@visibleForTesting
ExceptionTypeIdentifier get identifier => _identifier;
final ExceptionTypeIdentifier _identifier;

final Map<Type, String?> _knownExceptionTypes = {};

CachingExceptionTypeIdentifier(this._identifier);

@override
String? identifyType(dynamic throwable) {
final runtimeType = throwable.runtimeType;
if (_knownExceptionTypes.containsKey(runtimeType)) {
return _knownExceptionTypes[runtimeType];
}

final identifiedType = _identifier.identifyType(throwable);

if (identifiedType != null) {
_knownExceptionTypes[runtimeType] = identifiedType;
}

return identifiedType;
}
}
3 changes: 3 additions & 0 deletions dart/lib/src/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:meta/meta.dart';

import 'dart_exception_type_identifier.dart';
import 'metrics/metrics_api.dart';
import 'run_zoned_guarded_integration.dart';
import 'event_processor/enricher/enricher_event_processor.dart';
Expand Down Expand Up @@ -85,6 +86,8 @@ class Sentry {
options.addEventProcessor(EnricherEventProcessor(options));
options.addEventProcessor(ExceptionEventProcessor(options));
options.addEventProcessor(DeduplicationEventProcessor(options));

options.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier());
}

/// This method reads available environment variables and uses them
Expand Down
25 changes: 13 additions & 12 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import 'dart:async';
import 'dart:math';

import 'package:meta/meta.dart';
import 'utils/stacktrace_utils.dart';
import 'metrics/metric.dart';
import 'metrics/metrics_aggregator.dart';
import 'sentry_baggage.dart';
import 'sentry_attachment/sentry_attachment.dart';

import 'client_reports/client_report_recorder.dart';
import 'client_reports/discard_reason.dart';
import 'event_processor.dart';
import 'hint.dart';
import 'sentry_trace_context_header.dart';
import 'sentry_user_feedback.dart';
import 'transport/rate_limiter.dart';
import 'metrics/metric.dart';
import 'metrics/metrics_aggregator.dart';
import 'protocol.dart';
import 'scope.dart';
import 'sentry_attachment/sentry_attachment.dart';
import 'sentry_baggage.dart';
import 'sentry_envelope.dart';
import 'sentry_exception_factory.dart';
import 'sentry_options.dart';
import 'sentry_stack_trace_factory.dart';
import 'sentry_trace_context_header.dart';
import 'sentry_user_feedback.dart';
import 'transport/data_category.dart';
import 'transport/http_transport.dart';
import 'transport/noop_transport.dart';
import 'transport/rate_limiter.dart';
import 'transport/spotlight_http_transport.dart';
import 'transport/task_queue.dart';
import 'utils/isolate_utils.dart';
import 'utils/stacktrace_utils.dart';
import 'version.dart';
import 'sentry_envelope.dart';
import 'client_reports/client_report_recorder.dart';
import 'client_reports/discard_reason.dart';
import 'transport/data_category.dart';

/// Default value for [SentryUser.ipAddress]. It gets set when an event does not have
/// a user and IP address. Only applies if [SentryOptions.sendDefaultPii] is set
Expand Down
19 changes: 15 additions & 4 deletions dart/lib/src/sentry_exception_factory.dart
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
import 'utils/stacktrace_utils.dart';

import 'recursive_exception_cause_extractor.dart';
import 'protocol.dart';
import 'recursive_exception_cause_extractor.dart';
import 'sentry_options.dart';
import 'sentry_stack_trace_factory.dart';
import 'throwable_mechanism.dart';
import 'utils/stacktrace_utils.dart';

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

String errorTypeName = throwable.runtimeType.toString();

if (_options.enableExceptionTypeIdentification) {
for (final errorTypeIdentifier in _options.exceptionTypeIdentifiers) {
final identifiedErrorType = errorTypeIdentifier.identifyType(throwable);
if (identifiedErrorType != null) {
errorTypeName = identifiedErrorType;
break;
}
}
}

// if --obfuscate feature is enabled, 'type' won't be human readable.
// https://flutter.dev/docs/deployment/obfuscate#caveat
return SentryException(
type: (throwable.runtimeType).toString(),
type: errorTypeName,
value: value.isNotEmpty ? value : null,
mechanism: mechanism,
stackTrace: sentryStackTrace,
Expand Down
33 changes: 30 additions & 3 deletions dart/lib/src/sentry_options.dart
Original file line number Diff line number Diff line change
@@ -1,17 +1,17 @@
import 'dart:async';
import 'dart:developer';

import 'package:meta/meta.dart';
import 'package:http/http.dart';
import 'package:meta/meta.dart';

import '../sentry.dart';
import 'client_reports/client_report_recorder.dart';
import 'client_reports/noop_client_report_recorder.dart';
import 'sentry_exception_factory.dart';
import 'sentry_stack_trace_factory.dart';
import 'diagnostic_logger.dart';
import 'environment/environment_variables.dart';
import 'noop_client.dart';
import 'sentry_exception_factory.dart';
import 'sentry_stack_trace_factory.dart';
import 'transport/noop_transport.dart';
import 'version.dart';

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

/// Enables identification of exception types in obfuscated builds.
/// When true, the SDK will attempt to identify common exception types
/// to improve readability of obfuscated issue titles.
///
/// If you already have events with obfuscated issue titles this will change grouping.
///
/// Default: `true`
bool enableExceptionTypeIdentification = true;

final List<ExceptionTypeIdentifier> _exceptionTypeIdentifiers = [];

List<ExceptionTypeIdentifier> get exceptionTypeIdentifiers =>
List.unmodifiable(_exceptionTypeIdentifiers);

void addExceptionTypeIdentifierByIndex(
int index, ExceptionTypeIdentifier exceptionTypeIdentifier) {
_exceptionTypeIdentifiers.insert(
index, exceptionTypeIdentifier.withCache());
}

/// Adds an exception type identifier to the beginning of the list.
/// This ensures it is processed first and takes precedence over existing identifiers.
void prependExceptionTypeIdentifier(
ExceptionTypeIdentifier exceptionTypeIdentifier) {
addExceptionTypeIdentifierByIndex(0, exceptionTypeIdentifier);
}

/// The Spotlight configuration.
/// Disabled by default.
/// ```dart
Expand Down
Loading

0 comments on commit d593269

Please sign in to comment.