Skip to content

Commit 6edf8b8

Browse files
denrasebuenaflor
andauthored
v9: Parent-child relationship for the PlatformExceptions and Cause (#2803)
* add source to exception cause, populate source from sentry exception factory * add source for osError * revert sentry exception cause factory changes * extract soruce from android platform exceptions * add ExceptionGroupEventProcessor * format * reverse order according to rfc * iterate seperatley * update test * introduce parent/child relationship in exceptions, flatten exceptions according to rfc * mark methods as internal * add cl entry * fix integration test * fix cl * don’t try grouping if there are no children * sentry client builds correct hierarchy for exception causes * reumove unused test * attach missing thread to root exception * move flatten to event processor * format * handle copyWith usage * update exception grouping * Update dart/lib/src/sentry_exception_factory.dart remove newline Co-authored-by: Giancarlo Buenaflor <[email protected]> --------- Co-authored-by: Giancarlo Buenaflor <[email protected]>
1 parent 3f8357e commit 6edf8b8

17 files changed

+811
-146
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ Sentry.addFeatureFlag('my-feature', true);
1313
### Behavioral changes
1414

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

1719
### API Changes
1820

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import '../../event_processor.dart';
2+
import '../../protocol.dart';
3+
import '../../hint.dart';
4+
5+
/// Group exceptions into a flat list with references to hierarchy.
6+
class ExceptionGroupEventProcessor implements EventProcessor {
7+
@override
8+
SentryEvent? apply(SentryEvent event, Hint hint) {
9+
final sentryExceptions = event.exceptions ?? [];
10+
if (sentryExceptions.isEmpty) {
11+
return event;
12+
}
13+
final firstException = sentryExceptions.first;
14+
15+
if (sentryExceptions.length > 1 || firstException.exceptions == null) {
16+
// If already a list or no child exceptions, no grouping possible/needed.
17+
return event;
18+
} else {
19+
event.exceptions =
20+
firstException.flatten().reversed.toList(growable: false);
21+
return event;
22+
}
23+
}
24+
}
25+
26+
extension _SentryExceptionFlatten on SentryException {
27+
List<SentryException> flatten({int? parentId, int id = 0}) {
28+
final exceptions = this.exceptions ?? [];
29+
30+
final newMechanism = mechanism ?? Mechanism(type: "generic");
31+
newMechanism
32+
..type = id > 0 ? "chained" : newMechanism.type
33+
..parentId = parentId
34+
..exceptionId = id
35+
..isExceptionGroup = exceptions.isNotEmpty ? true : null;
36+
37+
mechanism = newMechanism;
38+
39+
var all = <SentryException>[];
40+
all.add(this);
41+
42+
if (exceptions.isNotEmpty) {
43+
final parentId = id;
44+
for (var exception in exceptions) {
45+
id++;
46+
final flattenedExceptions =
47+
exception.flatten(parentId: parentId, id: id);
48+
id = flattenedExceptions.lastOrNull?.mechanism?.exceptionId ?? id;
49+
all.addAll(flattenedExceptions);
50+
}
51+
}
52+
return all.toList(growable: false);
53+
}
54+
}

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

+35-23
Original file line numberDiff line numberDiff line change
@@ -43,16 +43,27 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor {
4343
SocketException exception,
4444
SentryEvent event,
4545
) {
46-
final address = exception.address;
4746
final osError = exception.osError;
47+
SentryException? osException;
48+
List<SentryException>? exceptions = event.exceptions;
49+
if (osError != null) {
50+
// OSError is the underlying error
51+
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
52+
// https://api.dart.dev/stable/dart-io/OSError-class.html
53+
osException = _sentryExceptionFromOsError(osError);
54+
final exception = event.exceptions?.firstOrNull;
55+
if (exception != null) {
56+
exception.addException(osException);
57+
} else {
58+
exceptions = [osException];
59+
}
60+
} else {
61+
exceptions = event.exceptions;
62+
}
63+
64+
final address = exception.address;
4865
if (address == null) {
49-
event.exceptions = [
50-
// OSError is the underlying error
51-
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
52-
// https://api.dart.dev/stable/dart-io/OSError-class.html
53-
if (osError != null) _sentryExceptionfromOsError(osError),
54-
...?event.exceptions,
55-
];
66+
event.exceptions = exceptions;
5667
return event;
5768
}
5869
SentryRequest? request;
@@ -71,15 +82,9 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor {
7182
}
7283
}
7384

74-
event.request = event.request ?? request;
75-
event.exceptions = [
76-
// OSError is the underlying error
77-
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
78-
// https://api.dart.dev/stable/dart-io/OSError-class.html
79-
if (osError != null) _sentryExceptionfromOsError(osError),
80-
...?event.exceptions,
81-
];
82-
return event;
85+
return event
86+
..request = event.request ?? request
87+
..exceptions = exceptions;
8388
}
8489

8590
// https://api.dart.dev/stable/dart-io/FileSystemException-class.html
@@ -88,18 +93,24 @@ class IoExceptionEventProcessor implements ExceptionEventProcessor {
8893
SentryEvent event,
8994
) {
9095
final osError = exception.osError;
91-
event.exceptions = [
96+
97+
if (osError != null) {
9298
// OSError is the underlying error
93-
// https://api.dart.dev/stable/dart-io/FileSystemException/osError.html
99+
// https://api.dart.dev/stable/dart-io/SocketException/osError.html
94100
// https://api.dart.dev/stable/dart-io/OSError-class.html
95-
if (osError != null) _sentryExceptionfromOsError(osError),
96-
...?event.exceptions,
97-
];
101+
final osException = _sentryExceptionFromOsError(osError);
102+
final exception = event.exceptions?.firstOrNull;
103+
if (exception != null) {
104+
exception.addException(osException);
105+
} else {
106+
event.exceptions = [osException];
107+
}
108+
}
98109
return event;
99110
}
100111
}
101112

102-
SentryException _sentryExceptionfromOsError(OSError osError) {
113+
SentryException _sentryExceptionFromOsError(OSError osError) {
103114
return SentryException(
104115
type: osError.runtimeType.toString(),
105116
value: osError.toString(),
@@ -110,6 +121,7 @@ SentryException _sentryExceptionfromOsError(OSError osError) {
110121
meta: {
111122
'errno': {'number': osError.errorCode},
112123
},
124+
source: 'osError',
113125
),
114126
);
115127
}

dart/lib/src/exception_cause.dart

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/// Holds inner exception and stackTrace combinations contained in other exceptions
22
class ExceptionCause {
3-
ExceptionCause(this.exception, this.stackTrace);
3+
ExceptionCause(this.exception, this.stackTrace, {this.source});
44

55
dynamic exception;
66
dynamic stackTrace;
7+
String? source;
78
}

dart/lib/src/protocol/sentry_exception.dart

+20-3
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@ class SentryException {
2626
dynamic throwable;
2727

2828
@internal
29-
final Map<String, dynamic>? unknown;
29+
Map<String, dynamic>? unknown;
30+
31+
List<SentryException>? _exceptions;
3032

3133
SentryException({
3234
required this.type,
@@ -86,10 +88,25 @@ class SentryException {
8688
type: type ?? this.type,
8789
value: value ?? this.value,
8890
module: module ?? this.module,
89-
stackTrace: stackTrace ?? this.stackTrace,
90-
mechanism: mechanism ?? this.mechanism,
91+
stackTrace: stackTrace ?? this.stackTrace?.copyWith(),
92+
mechanism: mechanism ?? this.mechanism?.copyWith(),
9193
threadId: threadId ?? this.threadId,
9294
throwable: throwable ?? this.throwable,
9395
unknown: unknown,
9496
);
97+
98+
@internal
99+
List<SentryException>? get exceptions =>
100+
_exceptions != null ? List.unmodifiable(_exceptions!) : null;
101+
102+
@internal
103+
set exceptions(List<SentryException>? value) {
104+
_exceptions = value;
105+
}
106+
107+
@internal
108+
void addException(SentryException exception) {
109+
_exceptions ??= [];
110+
_exceptions!.add(exception);
111+
}
95112
}

dart/lib/src/recursive_exception_cause_extractor.dart

+4-2
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,10 @@ class RecursiveExceptionCauseExtractor {
1616
final circularityDetector = <dynamic>{};
1717

1818
var currentException = exception;
19-
ExceptionCause? currentExceptionCause =
20-
ExceptionCause(exception, stackTrace);
19+
ExceptionCause? currentExceptionCause = ExceptionCause(
20+
exception,
21+
stackTrace,
22+
);
2123

2224
while (currentException != null &&
2325
currentExceptionCause != null &&

dart/lib/src/sentry.dart

+4
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'environment/environment_variables.dart';
77
import 'event_processor/deduplication_event_processor.dart';
88
import 'event_processor/enricher/enricher_event_processor.dart';
99
import 'event_processor/exception/exception_event_processor.dart';
10+
import 'event_processor/exception/exception_group_event_processor.dart';
1011
import 'hint.dart';
1112
import 'hub.dart';
1213
import 'hub_adapter.dart';
@@ -113,6 +114,9 @@ class Sentry {
113114
options.addEventProcessor(DeduplicationEventProcessor(options));
114115

115116
options.prependExceptionTypeIdentifier(DartExceptionTypeIdentifier());
117+
118+
// Added last to ensure all error events have correct parent/child relationships
119+
options.addEventProcessor(ExceptionGroupEventProcessor());
116120
}
117121

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

dart/lib/src/sentry_client.dart

+34-21
Original file line numberDiff line numberDiff line change
@@ -211,12 +211,13 @@ class SentryClient {
211211

212212
SentryEvent _prepareEvent(SentryEvent event, Hint hint,
213213
{dynamic stackTrace}) {
214-
event.serverName = event.serverName ?? _options.serverName;
215-
event.dist = event.dist ?? _options.dist;
216-
event.environment = event.environment ?? _options.environment;
217-
event.release = event.release ?? _options.release;
218-
event.sdk = event.sdk ?? _options.sdk;
219-
event.platform = event.platform ?? sdkPlatform(_options.platform.isWeb);
214+
event
215+
..serverName = event.serverName ?? _options.serverName
216+
..dist = event.dist ?? _options.dist
217+
..environment = event.environment ?? _options.environment
218+
..release = event.release ?? _options.release
219+
..sdk = event.sdk ?? _options.sdk
220+
..platform = event.platform ?? sdkPlatform(_options.platform.isWeb);
220221

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

237238
if (event.throwableMechanism != null) {
238-
final extractedExceptions = _exceptionFactory.extractor
239+
final extractedExceptionCauses = _exceptionFactory.extractor
239240
.flatten(event.throwableMechanism, stackTrace);
240241

241-
final sentryExceptions = <SentryException>[];
242+
SentryException? rootException;
243+
SentryException? currentException;
242244
final sentryThreads = <SentryThread>[];
243245

244-
for (final extractedException in extractedExceptions) {
246+
for (final extractedExceptionCause in extractedExceptionCauses) {
245247
var sentryException = _exceptionFactory.getSentryException(
246-
extractedException.exception,
247-
stackTrace: extractedException.stackTrace,
248+
extractedExceptionCause.exception,
249+
stackTrace: extractedExceptionCause.stackTrace,
248250
removeSentryFrames: hint.get(TypeCheckHint.currentStackTrace),
249251
);
252+
if (extractedExceptionCause.source != null) {
253+
var mechanism =
254+
sentryException.mechanism ?? Mechanism(type: "generic");
255+
256+
mechanism.source = extractedExceptionCause.source;
257+
sentryException.mechanism = mechanism;
258+
}
250259

251260
SentryThread? sentryThread;
252261

@@ -262,21 +271,25 @@ class SentryClient {
262271
);
263272
}
264273

265-
sentryExceptions.add(sentryException);
274+
rootException ??= sentryException;
275+
currentException?.addException(sentryException);
276+
currentException = sentryException;
277+
266278
if (sentryThread != null) {
267279
sentryThreads.add(sentryThread);
268280
}
269281
}
270282

271-
event.exceptions = [
272-
...?event.exceptions,
273-
...sentryExceptions,
274-
];
275-
event.threads = [
276-
...?event.threads,
277-
...sentryThreads,
278-
];
279-
return event;
283+
final exceptions = [...?event.exceptions];
284+
if (rootException != null) {
285+
exceptions.add(rootException);
286+
}
287+
return event
288+
..exceptions = exceptions
289+
..threads = [
290+
...?event.threads,
291+
...sentryThreads,
292+
];
280293
}
281294

282295
// The stacktrace is not part of an exception,

0 commit comments

Comments
 (0)