Skip to content

Commit 1ab55fc

Browse files
authored
Add a way to extract stacktraces from custom exception types (getsentry#1335)
1 parent 689d2fd commit 1ab55fc

11 files changed

+227
-4
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### Features
6+
7+
- Exception StackTrace Extractor ([#1335](https://github.com/getsentry/sentry-dart/pull/1335))
8+
59
### Dependencies
610

711
- Bump Cocoa SDK from v8.0.0 to v8.3.1 ([#1331](https://github.com/getsentry/sentry-dart/pull/1331))

dart/lib/sentry.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export 'src/type_check_hint.dart';
3535
// exception extraction
3636
export 'src/exception_cause_extractor.dart';
3737
export 'src/exception_cause.dart';
38+
export 'src/exception_stacktrace_extractor.dart';
3839
// Isolates
3940
export 'src/sentry_isolate_extension.dart';
4041
export 'src/sentry_isolate.dart';
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import 'protocol.dart';
2+
import 'sentry_options.dart';
3+
4+
/// Sentry handles [Error.stackTrace] by default. For other cases
5+
/// extend this abstract class and return a custom [StackTrace] of your
6+
/// exceptions.
7+
///
8+
/// Implementing an extractor and providing it through
9+
/// [SentryOptions.addExceptionStackTraceExtractor] will enable the framework to
10+
/// extract the inner stacktrace and add it to [SentryException] when no other
11+
/// stacktrace was provided while capturing the event.
12+
///
13+
/// For an example on how to use the API refer to dio/DioStackTraceExtractor or the
14+
/// code below:
15+
///
16+
/// ```dart
17+
/// class ExceptionWithInner {
18+
/// ExceptionWithInner(this.innerException, this.innerStackTrace);
19+
/// Object innerException;
20+
/// dynamic innerStackTrace;
21+
/// }
22+
///
23+
/// class ExceptionWithInnerStackTraceExtractor extends ExceptionStackTraceExtractor<ExceptionWithInner> {
24+
/// @override
25+
/// dynamic cause(ExceptionWithInner error) {
26+
/// return error.innerStackTrace;
27+
/// }
28+
/// }
29+
///
30+
/// options.addExceptionStackTraceExtractor(ExceptionWithInnerStackTraceExtractor());
31+
/// ```
32+
abstract class ExceptionStackTraceExtractor<T> {
33+
dynamic stackTrace(T error);
34+
Type get exceptionType => T;
35+
}

dart/lib/src/sentry_exception_factory.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ class SentryExceptionFactory {
3030
if (throwable is Error) {
3131
stackTrace ??= throwable.stackTrace;
3232
}
33+
stackTrace ??= _options
34+
.exceptionStackTraceExtractor(throwable.runtimeType)
35+
?.stackTrace(throwable);
36+
3337
// throwable.stackTrace is null if its an exception that was never thrown
3438
// hence we check again if stackTrace is null and if not, read the current stack trace
3539
// but only if attachStacktrace is enabled

dart/lib/src/sentry_options.dart

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -331,16 +331,28 @@ class SentryOptions {
331331
/// The default is 3 seconds.
332332
Duration? idleTimeout = Duration(seconds: 3);
333333

334-
final _extractorsByType = <Type, ExceptionCauseExtractor>{};
334+
final _causeExtractorsByType = <Type, ExceptionCauseExtractor>{};
335+
336+
final _stackTraceExtractorsByType = <Type, ExceptionStackTraceExtractor>{};
335337

336338
/// Returns a previously added [ExceptionCauseExtractor] by type
337339
ExceptionCauseExtractor? exceptionCauseExtractor(Type type) {
338-
return _extractorsByType[type];
340+
return _causeExtractorsByType[type];
339341
}
340342

341343
/// Adds [ExceptionCauseExtractor] in order to extract inner exceptions
342344
void addExceptionCauseExtractor(ExceptionCauseExtractor extractor) {
343-
_extractorsByType[extractor.exceptionType] = extractor;
345+
_causeExtractorsByType[extractor.exceptionType] = extractor;
346+
}
347+
348+
/// Returns a previously added [ExceptionStackTraceExtractor] by type
349+
ExceptionStackTraceExtractor? exceptionStackTraceExtractor(Type type) {
350+
return _stackTraceExtractorsByType[type];
351+
}
352+
353+
/// Adds [ExceptionStackTraceExtractor] in order to extract inner exceptions
354+
void addExceptionStackTraceExtractor(ExceptionStackTraceExtractor extractor) {
355+
_stackTraceExtractorsByType[extractor.exceptionType] = extractor;
344356
}
345357

346358
/// Changed SDK behaviour when set to true:

dart/test/sentry_client_test.dart

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -291,6 +291,32 @@ void main() {
291291
expect(capturedEvent.exceptions?[1].stackTrace!.frames.first.colNo, 9);
292292
});
293293

294+
test('should capture custom stacktrace', () async {
295+
fixture.options.addExceptionStackTraceExtractor(
296+
ExceptionWithStackTraceExtractor(),
297+
);
298+
299+
final stackTrace = StackTrace.fromString('''
300+
#0 baz (file:///pathto/test.dart:50:3)
301+
<asynchronous suspension>
302+
#1 bar (file:///pathto/test.dart:46:9)
303+
''');
304+
305+
exception = ExceptionWithStackTrace(stackTrace);
306+
307+
final client = fixture.getSut(attachStacktrace: true);
308+
await client.captureException(exception, stackTrace: null);
309+
310+
final capturedEnvelope = (fixture.transport).envelopes.first;
311+
final capturedEvent = await eventFromEnvelope(capturedEnvelope);
312+
313+
expect(capturedEvent.exceptions?[0].stackTrace, isNotNull);
314+
expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.fileName,
315+
'test.dart');
316+
expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.lineNo, 46);
317+
expect(capturedEvent.exceptions?[0].stackTrace!.frames.first.colNo, 9);
318+
});
319+
294320
test('should not capture cause stacktrace when attachStacktrace is false',
295321
() async {
296322
fixture.options.addExceptionCauseExtractor(
@@ -1638,3 +1664,16 @@ class ExceptionWithCauseExtractor
16381664
return ExceptionCause(error.cause, error.stackTrace);
16391665
}
16401666
}
1667+
1668+
class ExceptionWithStackTrace {
1669+
ExceptionWithStackTrace(this.stackTrace);
1670+
final StackTrace stackTrace;
1671+
}
1672+
1673+
class ExceptionWithStackTraceExtractor
1674+
extends ExceptionStackTraceExtractor<ExceptionWithStackTrace> {
1675+
@override
1676+
StackTrace? stackTrace(ExceptionWithStackTrace error) {
1677+
return error.stackTrace;
1678+
}
1679+
}

dart/test/sentry_exception_factory_test.dart

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,43 @@ void main() {
7373
expect(sentryException.stackTrace!.frames.first.fileName, 'test.dart');
7474
});
7575

76+
test('should extract stackTrace from custom exception', () {
77+
fixture.options
78+
.addExceptionStackTraceExtractor(CustomExceptionStackTraceExtractor());
79+
80+
SentryException sentryException;
81+
try {
82+
throw CustomException(StackTrace.fromString('''
83+
#0 baz (file:///pathto/test.dart:50:3)
84+
<asynchronous suspension>
85+
#1 bar (file:///pathto/test.dart:46:9)
86+
'''));
87+
} catch (err, _) {
88+
sentryException = fixture.getSut().getSentryException(
89+
err,
90+
);
91+
}
92+
93+
expect(sentryException.type, 'CustomException');
94+
expect(sentryException.stackTrace!.frames.first.lineNo, 46);
95+
expect(sentryException.stackTrace!.frames.first.colNo, 9);
96+
expect(sentryException.stackTrace!.frames.first.fileName, 'test.dart');
97+
});
98+
99+
test('should not fail when stackTrace property does not exist', () {
100+
SentryException sentryException;
101+
try {
102+
throw Object();
103+
} catch (err, _) {
104+
sentryException = fixture.getSut().getSentryException(
105+
err,
106+
);
107+
}
108+
109+
expect(sentryException.type, 'Object');
110+
expect(sentryException.stackTrace, isNotNull);
111+
});
112+
76113
test('getSentryException with not thrown Error and frames', () {
77114
final sentryException = fixture.getSut().getSentryException(
78115
CustomError(),
@@ -136,6 +173,20 @@ void main() {
136173

137174
class CustomError extends Error {}
138175

176+
class CustomException implements Exception {
177+
final StackTrace stackTrace;
178+
179+
CustomException(this.stackTrace);
180+
}
181+
182+
class CustomExceptionStackTraceExtractor
183+
extends ExceptionStackTraceExtractor<CustomException> {
184+
@override
185+
StackTrace? stackTrace(CustomException error) {
186+
return error.stackTrace;
187+
}
188+
}
189+
139190
class Fixture {
140191
final options = SentryOptions(dsn: fakeDsn);
141192

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:sentry/sentry.dart';
3+
4+
/// Extracts the inner stacktrace from [DioError]
5+
class DioStackTraceExtractor extends ExceptionStackTraceExtractor<DioError> {
6+
@override
7+
StackTrace? stackTrace(DioError error) {
8+
return error.stackTrace;
9+
}
10+
}

dio/lib/src/sentry_dio_extension.dart

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import 'package:dio/dio.dart';
22
import 'package:sentry/sentry.dart';
33
import 'dio_error_extractor.dart';
44
import 'dio_event_processor.dart';
5+
import 'dio_stacktrace_extractor.dart';
56
import 'failed_request_interceptor.dart';
67
import 'sentry_transformer.dart';
78
import 'sentry_dio_client_adapter.dart';
@@ -50,11 +51,16 @@ extension SentryDioExtension on Dio {
5051
// ignore: invalid_use_of_internal_member
5152
final options = hub.options;
5253

53-
// Add to get inner exception & stacktrace
54+
// Add to get inner exception
5455
if (options.exceptionCauseExtractor(DioError) == null) {
5556
options.addExceptionCauseExtractor(DioErrorExtractor());
5657
}
5758

59+
// Add to get inner stacktrace
60+
if (options.exceptionStackTraceExtractor(DioError) == null) {
61+
options.addExceptionStackTraceExtractor(DioStackTraceExtractor());
62+
}
63+
5864
// Add DioEventProcessor when it's not already present
5965
if (options.eventProcessors.whereType<DioEventProcessor>().isEmpty) {
6066
options.sdk.addIntegration('sentry_dio');
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import 'package:dio/dio.dart';
2+
import 'package:sentry_dio/src/dio_stacktrace_extractor.dart';
3+
import 'package:test/test.dart';
4+
5+
void main() {
6+
late Fixture fixture;
7+
8+
setUp(() {
9+
fixture = Fixture();
10+
});
11+
12+
group(DioStackTraceExtractor, () {
13+
test('extracts stacktrace', () {
14+
final sut = fixture.getSut();
15+
final exception = Exception('foo bar');
16+
final stacktrace = StackTrace.current;
17+
18+
final dioError = DioError(
19+
error: exception,
20+
requestOptions: RequestOptions(path: '/foo/bar'),
21+
stackTrace: stacktrace,
22+
);
23+
24+
final result = sut.stackTrace(dioError);
25+
26+
expect(result, stacktrace);
27+
});
28+
29+
test('extracts nothing with missing stacktrace', () {
30+
final sut = fixture.getSut();
31+
final exception = Exception('foo bar');
32+
33+
final dioError = DioError(
34+
error: exception,
35+
requestOptions: RequestOptions(path: '/foo/bar'),
36+
);
37+
38+
final result = sut.stackTrace(dioError);
39+
40+
expect(result, isNull);
41+
});
42+
});
43+
}
44+
45+
class Fixture {
46+
DioStackTraceExtractor getSut() {
47+
return DioStackTraceExtractor();
48+
}
49+
}

0 commit comments

Comments
 (0)