Skip to content

Commit d0476e1

Browse files
authored
Fix truncated stacktraces in unhandled errors (#2152)
* Fix stacktrace * Fix stacktrace * Update * Update changelog * Add test cases * formatting * formatting * Fix await
1 parent dd76eef commit d0476e1

File tree

5 files changed

+77
-10
lines changed

5 files changed

+77
-10
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@
3030

3131
### Fixes
3232

33+
- Capture meaningful stack traces when unhandled errors have empty or missing stack traces ([#2152](https://github.com/getsentry/sentry-dart/pull/2152))
34+
- This will affect grouping for unhandled errors that have empty or missing stack traces.
3335
- Fix sentry_drift compatibility with Drift 2.19.0 ([#2162](https://github.com/getsentry/sentry-dart/pull/2162))
3436
- App starts hanging for 30s ([#2140](https://github.com/getsentry/sentry-dart/pull/2140))
3537
- Time out for app start info retrieval has been reduced to 10s

flutter/lib/src/integrations/flutter_error_integration.dart

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ import 'package:flutter/foundation.dart';
22
import 'package:sentry/sentry.dart';
33
import '../sentry_flutter_options.dart';
44

5+
// ignore: implementation_imports
6+
import 'package:sentry/src/utils/stacktrace_utils.dart';
7+
58
/// Integration that capture errors on the [FlutterError.onError] handler.
69
///
710
/// Remarks:
@@ -77,7 +80,8 @@ class FlutterErrorIntegration implements Integration<SentryFlutterOptions> {
7780
);
7881

7982
await hub.captureEvent(event,
80-
stackTrace: errorDetails.stack,
83+
// ignore: invalid_use_of_internal_member
84+
stackTrace: errorDetails.stack ?? getCurrentStackTrace(),
8185
hint:
8286
Hint.withMap({TypeCheckHint.syntheticException: errorDetails}));
8387
// we don't call Zone.current.handleUncaughtError because we'd like

flutter/lib/src/integrations/on_error_integration.dart

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ import 'package:flutter/widgets.dart';
44
import 'package:sentry/sentry.dart';
55
import '../sentry_flutter_options.dart';
66

7+
// ignore: implementation_imports
8+
import 'package:sentry/src/utils/stacktrace_utils.dart';
9+
710
typedef ErrorCallback = bool Function(Object exception, StackTrace stackTrace);
811

912
/// Integration which captures `PlatformDispatcher.onError`
@@ -74,6 +77,11 @@ class OnErrorIntegration implements Integration<SentryFlutterOptions> {
7477
(scope) => scope.span?.status ??= const SpanStatus.internalError(),
7578
);
7679

80+
if (stackTrace == StackTrace.empty) {
81+
// ignore: invalid_use_of_internal_member
82+
stackTrace = getCurrentStackTrace();
83+
}
84+
7785
// unawaited future
7886
hub.captureEvent(event, stackTrace: stackTrace);
7987

flutter/test/integrations/flutter_error_integration_test.dart

Lines changed: 43 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ void main() {
1818
void _mockValues() {
1919
when(fixture.hub.configureScope(captureAny)).thenAnswer((_) {});
2020

21-
when(fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')))
21+
when(fixture.hub.captureEvent(captureAny,
22+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')))
2223
.thenAnswer((_) => Future.value(SentryId.empty()));
2324

2425
when(fixture.hub.options).thenReturn(fixture.options);
@@ -63,7 +64,11 @@ void main() {
6364
_reportError(exception: exception);
6465

6566
final event = verify(
66-
await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')),
67+
await fixture.hub.captureEvent(
68+
captureAny,
69+
hint: anyNamed('hint'),
70+
stackTrace: anyNamed('stackTrace'),
71+
),
6772
).captured.first as SentryEvent;
6873

6974
expect(event.level, SentryLevel.fatal);
@@ -95,7 +100,8 @@ void main() {
95100
_reportError(exception: StateError('error'), optionalDetails: details);
96101

97102
final event = verify(
98-
await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')),
103+
await fixture.hub.captureEvent(captureAny,
104+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')),
99105
).captured.first as SentryEvent;
100106

101107
expect(event.level, SentryLevel.fatal);
@@ -119,7 +125,8 @@ void main() {
119125
_reportError(exception: StateError('error'), optionalDetails: details);
120126

121127
final event = verify(
122-
await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')),
128+
await fixture.hub.captureEvent(captureAny,
129+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')),
123130
).captured.first as SentryEvent;
124131

125132
expect(event.level, SentryLevel.fatal);
@@ -141,7 +148,9 @@ void main() {
141148
_reportError(handler: defaultError);
142149

143150
verify(
144-
await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')));
151+
await fixture.hub.captureEvent(captureAny,
152+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')),
153+
);
145154

146155
expect(called, true);
147156
});
@@ -166,8 +175,10 @@ void main() {
166175

167176
FlutterError.reportError(details);
168177

169-
verify(await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')))
170-
.called(1);
178+
verify(
179+
await fixture.hub.captureEvent(captureAny,
180+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')),
181+
).called(1);
171182

172183
expect(numberOfDefaultCalls, 1);
173184
});
@@ -200,6 +211,26 @@ void main() {
200211
expect(FlutterError.onError, afterIntegrationOnError);
201212
});
202213

214+
test('captureEvent never uses an empty or null stack trace', () async {
215+
final exception = StateError('error');
216+
final details = FlutterErrorDetails(
217+
exception: exception,
218+
stack: null, // Explicitly set stack to null
219+
);
220+
221+
_reportError(optionalDetails: details);
222+
223+
final captured = verify(
224+
await fixture.hub.captureEvent(captureAny,
225+
hint: anyNamed('hint'), stackTrace: captureAnyNamed('stackTrace')),
226+
).captured;
227+
228+
final stackTrace = captured[1] as StackTrace?;
229+
230+
expect(stackTrace, isNotNull);
231+
expect(stackTrace.toString(), isNotEmpty);
232+
});
233+
203234
test('do not capture if silent error', () async {
204235
_reportError(silent: true);
205236

@@ -211,7 +242,9 @@ void main() {
211242
_reportError(silent: true);
212243

213244
verify(
214-
await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')));
245+
await fixture.hub.captureEvent(captureAny,
246+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')),
247+
);
215248
});
216249

217250
test('adds integration', () {
@@ -255,7 +288,8 @@ void main() {
255288
_reportError(exception: exception);
256289

257290
final event = verify(
258-
await fixture.hub.captureEvent(captureAny, hint: anyNamed('hint')),
291+
await fixture.hub.captureEvent(captureAny,
292+
hint: anyNamed('hint'), stackTrace: anyNamed('stackTrace')),
259293
).captured.first as SentryEvent;
260294

261295
expect(event.level, SentryLevel.error);

flutter/test/integrations/on_error_integration_test.dart

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,25 @@ void main() {
9595
expect(throwableMechanism.mechanism.handled, false);
9696
});
9797

98+
test('captureEvent never uses an empty or null stack trace', () async {
99+
final exception = StateError('error');
100+
_reportError(
101+
exception: exception,
102+
stackTrace: StackTrace.current,
103+
onErrorReturnValue: false,
104+
);
105+
106+
final captured = verify(
107+
await fixture.hub.captureEvent(captureAny,
108+
hint: anyNamed('hint'), stackTrace: captureAnyNamed('stackTrace')),
109+
).captured;
110+
111+
final stackTrace = captured[1] as StackTrace?;
112+
113+
expect(stackTrace, isNotNull);
114+
expect(stackTrace.toString(), isNotEmpty);
115+
});
116+
98117
test('calls default error', () async {
99118
var called = false;
100119
final defaultError = (_, __) {

0 commit comments

Comments
 (0)