Skip to content

v9: Parent-child relationship for the PlatformExceptions and Cause #2803

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

Merged
merged 30 commits into from
Apr 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
bd65dec
add source to exception cause, populate source from sentry exception …
denrase Mar 18, 2025
8488663
Merge branch 'main' into enha/rel-platfomr-cause
denrase Mar 18, 2025
b950043
add source for osError
denrase Mar 18, 2025
3486958
revert sentry exception cause factory changes
denrase Mar 18, 2025
fd73eaf
extract soruce from android platform exceptions
denrase Mar 18, 2025
07087aa
add ExceptionGroupEventProcessor
denrase Mar 18, 2025
2e189f2
format
denrase Mar 18, 2025
27ab973
reverse order according to rfc
denrase Mar 19, 2025
c888e40
iterate seperatley
denrase Mar 19, 2025
d317c84
update test
denrase Mar 19, 2025
9a23c4a
introduce parent/child relationship in exceptions, flatten exceptions…
denrase Mar 19, 2025
83081be
Merge branch 'main' into enha/rel-platfomr-cause
denrase Mar 27, 2025
576672c
mark methods as internal
denrase Mar 27, 2025
28a7e06
add cl entry
denrase Mar 27, 2025
d74c081
fix integration test
denrase Mar 27, 2025
4f6a74f
fix cl
denrase Mar 27, 2025
5e0236b
don’t try grouping if there are no children
denrase Mar 27, 2025
31c1a24
sentry client builds correct hierarchy for exception causes
denrase Mar 27, 2025
277bc21
reumove unused test
denrase Mar 27, 2025
a57fa2b
attach missing thread to root exception
denrase Mar 27, 2025
a3e6104
Merge branch 'main' into enha/rel-platfomr-cause
denrase Apr 2, 2025
23d5f24
move flatten to event processor
denrase Apr 2, 2025
972a9b7
Merge branch 'main' into enha/rel-platfomr-cause
denrase Apr 7, 2025
6fdbf5b
format
denrase Apr 7, 2025
4adb170
Merge branch 'main' into enha/rel-platfomr-cause
denrase Apr 7, 2025
a19c9aa
handle copyWith usage
denrase Apr 8, 2025
3a6af68
update exception grouping
denrase Apr 9, 2025
e26f4d4
Merge branch 'main' into enha/rel-platfomr-cause
denrase Apr 9, 2025
cd9e4ae
Update dart/lib/src/sentry_exception_factory.dart
denrase Apr 10, 2025
80e7f9e
Merge branch 'main' into enha/rel-platfomr-cause
buenaflor Apr 10, 2025
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ Sentry.addFeatureFlag('my-feature', true);
### Behavioral changes

- Set log level to `warning` by default when `debug = true` ([#2836](https://github.com/getsentry/sentry-dart/pull/2836))
- Parent-child relationship for the PlatformExceptions and Cause ([#2803](https://github.com/getsentry/sentry-dart/pull/2803))
- Improves and changes exception grouping

### API Changes

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import '../../event_processor.dart';
import '../../protocol.dart';
import '../../hint.dart';

/// Group exceptions into a flat list with references to hierarchy.
class ExceptionGroupEventProcessor implements EventProcessor {
@override
SentryEvent? apply(SentryEvent event, Hint hint) {
final sentryExceptions = event.exceptions ?? [];
if (sentryExceptions.isEmpty) {
return event;
}
final firstException = sentryExceptions.first;

if (sentryExceptions.length > 1 || firstException.exceptions == null) {
// If already a list or no child exceptions, no grouping possible/needed.
return event;
} else {
event.exceptions =
firstException.flatten().reversed.toList(growable: false);
return event;
}
}
}

extension _SentryExceptionFlatten on SentryException {
List<SentryException> flatten({int? parentId, int id = 0}) {
final exceptions = this.exceptions ?? [];

final newMechanism = mechanism ?? Mechanism(type: "generic");
newMechanism
..type = id > 0 ? "chained" : newMechanism.type
..parentId = parentId
..exceptionId = id
..isExceptionGroup = exceptions.isNotEmpty ? true : null;

mechanism = newMechanism;

var all = <SentryException>[];
all.add(this);

if (exceptions.isNotEmpty) {
final parentId = id;
for (var exception in exceptions) {
id++;
final flattenedExceptions =
exception.flatten(parentId: parentId, id: id);
id = flattenedExceptions.lastOrNull?.mechanism?.exceptionId ?? id;
all.addAll(flattenedExceptions);
}
}
return all.toList(growable: false);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -43,16 +43,27 @@
SocketException exception,
SentryEvent event,
) {
final address = exception.address;
final osError = exception.osError;
SentryException? osException;
List<SentryException>? exceptions = event.exceptions;
if (osError != null) {
// OSError is the underlying error
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
// https://api.dart.dev/stable/dart-io/OSError-class.html
osException = _sentryExceptionFromOsError(osError);
final exception = event.exceptions?.firstOrNull;
if (exception != null) {
exception.addException(osException);
} else {
exceptions = [osException];

Check warning on line 58 in dart/lib/src/event_processor/exception/io_exception_event_processor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/event_processor/exception/io_exception_event_processor.dart#L58

Added line #L58 was not covered by tests
}
} else {
exceptions = event.exceptions;

Check warning on line 61 in dart/lib/src/event_processor/exception/io_exception_event_processor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/event_processor/exception/io_exception_event_processor.dart#L61

Added line #L61 was not covered by tests
}

final address = exception.address;
if (address == null) {
event.exceptions = [
// OSError is the underlying error
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
// https://api.dart.dev/stable/dart-io/OSError-class.html
if (osError != null) _sentryExceptionfromOsError(osError),
...?event.exceptions,
];
event.exceptions = exceptions;

Check warning on line 66 in dart/lib/src/event_processor/exception/io_exception_event_processor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/event_processor/exception/io_exception_event_processor.dart#L66

Added line #L66 was not covered by tests
return event;
}
SentryRequest? request;
Expand All @@ -71,15 +82,9 @@
}
}

event.request = event.request ?? request;
event.exceptions = [
// OSError is the underlying error
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
// https://api.dart.dev/stable/dart-io/OSError-class.html
if (osError != null) _sentryExceptionfromOsError(osError),
...?event.exceptions,
];
return event;
return event
..request = event.request ?? request
..exceptions = exceptions;
}

// https://api.dart.dev/stable/dart-io/FileSystemException-class.html
Expand All @@ -88,18 +93,24 @@
SentryEvent event,
) {
final osError = exception.osError;
event.exceptions = [

if (osError != null) {
// OSError is the underlying error
// https://api.dart.dev/stable/dart-io/FileSystemException/osError.html
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
// https://api.dart.dev/stable/dart-io/OSError-class.html
if (osError != null) _sentryExceptionfromOsError(osError),
...?event.exceptions,
];
final osException = _sentryExceptionFromOsError(osError);
final exception = event.exceptions?.firstOrNull;
if (exception != null) {
exception.addException(osException);
} else {
event.exceptions = [osException];

Check warning on line 106 in dart/lib/src/event_processor/exception/io_exception_event_processor.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/event_processor/exception/io_exception_event_processor.dart#L106

Added line #L106 was not covered by tests
}
}
return event;
}
}

SentryException _sentryExceptionfromOsError(OSError osError) {
SentryException _sentryExceptionFromOsError(OSError osError) {
return SentryException(
type: osError.runtimeType.toString(),
value: osError.toString(),
Expand All @@ -110,6 +121,7 @@
meta: {
'errno': {'number': osError.errorCode},
},
source: 'osError',
),
);
}
3 changes: 2 additions & 1 deletion dart/lib/src/exception_cause.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
/// Holds inner exception and stackTrace combinations contained in other exceptions
class ExceptionCause {
ExceptionCause(this.exception, this.stackTrace);
ExceptionCause(this.exception, this.stackTrace, {this.source});

dynamic exception;
dynamic stackTrace;
String? source;
}
23 changes: 20 additions & 3 deletions dart/lib/src/protocol/sentry_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@
dynamic throwable;

@internal
final Map<String, dynamic>? unknown;
Map<String, dynamic>? unknown;

List<SentryException>? _exceptions;

SentryException({
required this.type,
Expand Down Expand Up @@ -86,10 +88,25 @@
type: type ?? this.type,
value: value ?? this.value,
module: module ?? this.module,
stackTrace: stackTrace ?? this.stackTrace,
mechanism: mechanism ?? this.mechanism,
stackTrace: stackTrace ?? this.stackTrace?.copyWith(),
mechanism: mechanism ?? this.mechanism?.copyWith(),
Comment on lines +91 to +92
Copy link
Contributor

Choose a reason for hiding this comment

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

do we need the copyWith?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

For the tests, yeah. There we use copy with and without this, we'd just share the same instance of stackTrace/mecahnism between multiple tests, now that it's immutable.

Copy link
Contributor

Choose a reason for hiding this comment

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

is it possible to fix this? since copyWith is deprecated we will remove it at some point

but if it's too much work it's fine, we can do it when we eventually remove it

Copy link
Contributor

Choose a reason for hiding this comment

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

Let's leave it for now and handle this some other time

threadId: threadId ?? this.threadId,
throwable: throwable ?? this.throwable,
unknown: unknown,
);

@internal
List<SentryException>? get exceptions =>
_exceptions != null ? List.unmodifiable(_exceptions!) : null;

@internal

Check warning on line 102 in dart/lib/src/protocol/sentry_exception.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/protocol/sentry_exception.dart#L102

Added line #L102 was not covered by tests
set exceptions(List<SentryException>? value) {
_exceptions = value;

Check warning on line 104 in dart/lib/src/protocol/sentry_exception.dart

View check run for this annotation

Codecov / codecov/patch

dart/lib/src/protocol/sentry_exception.dart#L104

Added line #L104 was not covered by tests
}

@internal
void addException(SentryException exception) {
_exceptions ??= [];
_exceptions!.add(exception);
}
}
6 changes: 4 additions & 2 deletions dart/lib/src/recursive_exception_cause_extractor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,10 @@ class RecursiveExceptionCauseExtractor {
final circularityDetector = <dynamic>{};

var currentException = exception;
ExceptionCause? currentExceptionCause =
ExceptionCause(exception, stackTrace);
ExceptionCause? currentExceptionCause = ExceptionCause(
exception,
stackTrace,
);

while (currentException != null &&
currentExceptionCause != null &&
Expand Down
4 changes: 4 additions & 0 deletions dart/lib/src/sentry.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import 'environment/environment_variables.dart';
import 'event_processor/deduplication_event_processor.dart';
import 'event_processor/enricher/enricher_event_processor.dart';
import 'event_processor/exception/exception_event_processor.dart';
import 'event_processor/exception/exception_group_event_processor.dart';
import 'hint.dart';
import 'hub.dart';
import 'hub_adapter.dart';
Expand Down Expand Up @@ -113,6 +114,9 @@ class Sentry {
options.addEventProcessor(DeduplicationEventProcessor(options));

options.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier());

// Added last to ensure all error events have correct parent/child relationships
options.addEventProcessor(ExceptionGroupEventProcessor());
}

/// This method reads available environment variables and uses them
Expand Down
55 changes: 34 additions & 21 deletions dart/lib/src/sentry_client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -211,12 +211,13 @@ class SentryClient {

SentryEvent _prepareEvent(SentryEvent event, Hint hint,
{dynamic stackTrace}) {
event.serverName = event.serverName ?? _options.serverName;
event.dist = event.dist ?? _options.dist;
event.environment = event.environment ?? _options.environment;
event.release = event.release ?? _options.release;
event.sdk = event.sdk ?? _options.sdk;
event.platform = event.platform ?? sdkPlatform(_options.platform.isWeb);
event
..serverName = event.serverName ?? _options.serverName
..dist = event.dist ?? _options.dist
..environment = event.environment ?? _options.environment
..release = event.release ?? _options.release
..sdk = event.sdk ?? _options.sdk
..platform = event.platform ?? sdkPlatform(_options.platform.isWeb);

if (event is SentryTransaction) {
return event;
Expand All @@ -235,18 +236,26 @@ class SentryClient {
final isolateId = isolateName?.hashCode;

if (event.throwableMechanism != null) {
final extractedExceptions = _exceptionFactory.extractor
final extractedExceptionCauses = _exceptionFactory.extractor
.flatten(event.throwableMechanism, stackTrace);

final sentryExceptions = <SentryException>[];
SentryException? rootException;
SentryException? currentException;
final sentryThreads = <SentryThread>[];

for (final extractedException in extractedExceptions) {
for (final extractedExceptionCause in extractedExceptionCauses) {
var sentryException = _exceptionFactory.getSentryException(
extractedException.exception,
stackTrace: extractedException.stackTrace,
extractedExceptionCause.exception,
stackTrace: extractedExceptionCause.stackTrace,
removeSentryFrames: hint.get(TypeCheckHint.currentStackTrace),
);
if (extractedExceptionCause.source != null) {
var mechanism =
sentryException.mechanism ?? Mechanism(type: "generic");

mechanism.source = extractedExceptionCause.source;
sentryException.mechanism = mechanism;
}

SentryThread? sentryThread;

Expand All @@ -262,21 +271,25 @@ class SentryClient {
);
}

sentryExceptions.add(sentryException);
rootException ??= sentryException;
currentException?.addException(sentryException);
currentException = sentryException;

if (sentryThread != null) {
sentryThreads.add(sentryThread);
}
}

event.exceptions = [
...?event.exceptions,
...sentryExceptions,
];
event.threads = [
...?event.threads,
...sentryThreads,
];
return event;
final exceptions = [...?event.exceptions];
if (rootException != null) {
exceptions.add(rootException);
}
return event
..exceptions = exceptions
..threads = [
...?event.threads,
...sentryThreads,
];
}

// The stacktrace is not part of an exception,
Expand Down
Loading
Loading