Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement error type identifier to mitigate obfuscated Flutter issue titles #2170

Merged
merged 26 commits into from
Jul 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it's great that we have this option 👍 I would put this note one bullet above, so that it's connected to the callout about being opt out and that it changes (fixes!) grouping

- 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
Loading