-
-
Notifications
You must be signed in to change notification settings - Fork 277
Feat: Error Cause Extractor #1198
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
Changes from all commits
88e39d2
ba00651
81394d1
620e917
1c7d835
2584bad
e8384ca
b4fb217
077aff4
a1acbf3
fcd1a38
34e9e8a
84f1104
0c15cd2
413ddd0
4af4d07
5159821
983bdf3
bde4df0
4bd8ad4
406e493
61900d4
5d78242
b2abac7
2e65a9e
4f37d19
72cfb5b
db64952
dcbc09f
4938f3a
cce32de
22196e6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| class ExceptionCause { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Let's add docs for public classes and properties, that apply to some other files as well. |
||
| ExceptionCause(this.exception, this.stackTrace); | ||
|
|
||
| dynamic exception; | ||
| dynamic stackTrace; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| import 'exception_cause.dart'; | ||
| import 'sentry_options.dart'; | ||
| import 'throwable_mechanism.dart'; | ||
|
|
||
| abstract class ExceptionCauseExtractor<T> { | ||
| ExceptionCause? cause(T error); | ||
| Type get exceptionType => T; | ||
| } | ||
|
|
||
| class RecursiveExceptionCauseExtractor { | ||
| RecursiveExceptionCauseExtractor(this._options); | ||
|
|
||
| final SentryOptions _options; | ||
|
|
||
| List<ExceptionCause> flatten(exception, stackTrace) { | ||
| final allExceptionCauses = <ExceptionCause>[]; | ||
| final circularityDetector = <dynamic>{}; | ||
|
|
||
| var currentException = exception; | ||
| ExceptionCause? currentExceptionCause = | ||
| ExceptionCause(exception, stackTrace); | ||
|
|
||
| while (currentException != null && | ||
| currentExceptionCause != null && | ||
| circularityDetector.add(currentException)) { | ||
| allExceptionCauses.add(currentExceptionCause); | ||
|
|
||
| final extractionSourceSource = currentException is ThrowableMechanism | ||
| ? currentException.throwable | ||
| : currentException; | ||
|
|
||
| final extractor = | ||
| _options.exceptionCauseExtractor(extractionSourceSource.runtimeType); | ||
|
|
||
| currentExceptionCause = extractor?.cause(extractionSourceSource); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Better to wrap that with a try-catch, otherwise, we will silently swallow the exception in case the extract has a bug. |
||
| currentException = currentExceptionCause?.exception; | ||
| } | ||
| return allExceptionCauses; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import 'package:sentry/src/exception_cause.dart'; | ||
| import 'package:sentry/src/exception_cause_extractor.dart'; | ||
| import 'package:sentry/src/protocol/mechanism.dart'; | ||
| import 'package:sentry/src/sentry_options.dart'; | ||
| import 'package:sentry/src/throwable_mechanism.dart'; | ||
| import 'package:test/test.dart'; | ||
| import 'mocks.dart'; | ||
|
|
||
| void main() { | ||
| late Fixture fixture; | ||
|
|
||
| setUp(() { | ||
| fixture = Fixture(); | ||
| }); | ||
|
|
||
| test('flatten', () { | ||
| final errorC = ExceptionC(); | ||
| final errorB = ExceptionB(errorC); | ||
| final errorA = ExceptionA(errorB); | ||
|
|
||
| fixture.options.addExceptionCauseExtractor( | ||
| ExceptionACauseExtractor(), | ||
| ); | ||
|
|
||
| fixture.options.addExceptionCauseExtractor( | ||
| ExceptionBCauseExtractor(), | ||
| ); | ||
|
|
||
| final sut = fixture.getSut(); | ||
|
|
||
| final flattened = sut.flatten(errorA, null); | ||
| final actual = flattened.map((exceptionCause) => exceptionCause.exception); | ||
| expect(actual, [errorA, errorB, errorC]); | ||
| }); | ||
|
|
||
| test('flatten breaks circularity', () { | ||
| final a = ExceptionCircularA(); | ||
| final b = ExceptionCircularB(); | ||
| a.other = b; | ||
| b.other = a; | ||
|
|
||
| fixture.options.addExceptionCauseExtractor( | ||
| ExceptionCircularAExtractor(), | ||
| ); | ||
|
|
||
| fixture.options.addExceptionCauseExtractor( | ||
| ExceptionCircularBExtractor(), | ||
| ); | ||
|
|
||
| final sut = fixture.getSut(); | ||
|
|
||
| final flattened = sut.flatten(a, null); | ||
| final actual = flattened.map((exceptionCause) => exceptionCause.exception); | ||
|
|
||
| expect(actual, [a, b]); | ||
| }); | ||
|
|
||
| test('flatten preserves throwable mechanism', () { | ||
| final errorC = ExceptionC(); | ||
| final errorB = ExceptionB(errorC); | ||
| final errorA = ExceptionA(errorB); | ||
|
|
||
| fixture.options.addExceptionCauseExtractor( | ||
| ExceptionACauseExtractor(), | ||
| ); | ||
|
|
||
| fixture.options.addExceptionCauseExtractor( | ||
| ExceptionBCauseExtractor(), | ||
| ); | ||
|
|
||
| final mechanism = Mechanism(type: "foo"); | ||
| final throwableMechanism = ThrowableMechanism(mechanism, errorA); | ||
|
|
||
| final sut = fixture.getSut(); | ||
| final flattened = sut.flatten(throwableMechanism, null); | ||
|
|
||
| final actual = flattened.map((exceptionCause) => exceptionCause.exception); | ||
| expect(actual, [throwableMechanism, errorB, errorC]); | ||
| }); | ||
| } | ||
|
|
||
| class Fixture { | ||
| final options = SentryOptions(dsn: fakeDsn); | ||
|
|
||
| RecursiveExceptionCauseExtractor getSut() { | ||
| return RecursiveExceptionCauseExtractor(options); | ||
| } | ||
| } | ||
|
|
||
| class ExceptionA { | ||
| ExceptionA(this.other); | ||
| final ExceptionB? other; | ||
| } | ||
|
|
||
| class ExceptionB { | ||
| ExceptionB(this.anotherOther); | ||
| final ExceptionC? anotherOther; | ||
| } | ||
|
|
||
| class ExceptionC { | ||
| // I am empty inside | ||
| } | ||
|
|
||
| class ExceptionACauseExtractor extends ExceptionCauseExtractor<ExceptionA> { | ||
| @override | ||
| ExceptionCause? cause(ExceptionA error) { | ||
| return ExceptionCause(error.other, null); | ||
| } | ||
| } | ||
|
|
||
| class ExceptionBCauseExtractor extends ExceptionCauseExtractor<ExceptionB> { | ||
| @override | ||
| ExceptionCause? cause(ExceptionB error) { | ||
| return ExceptionCause(error.anotherOther, null); | ||
| } | ||
| } | ||
|
|
||
| class ExceptionCircularA { | ||
| ExceptionCircularB? other; | ||
| } | ||
|
|
||
| class ExceptionCircularB { | ||
| ExceptionCircularA? other; | ||
| } | ||
|
|
||
| class ExceptionCircularAExtractor | ||
| extends ExceptionCauseExtractor<ExceptionCircularA> { | ||
| @override | ||
| ExceptionCause? cause(ExceptionCircularA error) { | ||
| return ExceptionCause(error.other, null); | ||
| } | ||
| } | ||
|
|
||
| class ExceptionCircularBExtractor | ||
| extends ExceptionCauseExtractor<ExceptionCircularB> { | ||
| @override | ||
| ExceptionCause? cause(ExceptionCircularB error) { | ||
| return ExceptionCause(error.other, null); | ||
| } | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.