Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,20 +43,20 @@ class DartExceptionProcessor {
StackTrace? effectiveStackTrace = stackTrace;
bool isGeneratedStackTrace = false;

// If it's an Error, try to use its built-in stackTrace
if (currentError is Error) {
effectiveStackTrace ??= currentError.stackTrace;
// If no usable trace was supplied, try the error's own stackTrace
if (effectiveStackTrace == null ||
effectiveStackTrace.toString().trim().isEmpty) {
effectiveStackTrace = _extractOwnStackTrace(currentError);
}

// If still null or empty, get current stack trace
if (effectiveStackTrace == null ||
effectiveStackTrace == StackTrace.empty) {
// If still null, get current stack trace
if (effectiveStackTrace == null) {
effectiveStackTrace = stackTraceProvider?.call() ?? StackTrace.current;
isGeneratedStackTrace = true; // Flag to remove top PostHog frames
}

// Check if we still have an empty stack trace
final hasValidStackTrace = effectiveStackTrace != StackTrace.empty;
final hasValidStackTrace = effectiveStackTrace.toString().trim().isNotEmpty;

// Process primary exception
final frames = hasValidStackTrace
Expand Down Expand Up @@ -268,7 +268,7 @@ class DartExceptionProcessor {
stackTrace = error.stackTrace;
}

if (stackTrace == null || stackTrace == StackTrace.empty) {
if (stackTrace == null || stackTrace.toString().trim().isEmpty) {
return null;
}
return stackTrace;
Expand Down
108 changes: 108 additions & 0 deletions posthog_flutter/test/empty_stack_fallback_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import 'dart:async';

import 'package:flutter_test/flutter_test.dart';
import 'package:posthog_flutter/src/error_tracking/dart_exception_processor.dart';
import 'package:posthog_flutter/src/error_tracking/posthog_exception.dart';
import 'package:stack_trace/stack_trace.dart';

Never _throwOriginalError() => throw StateError('original failure');

void main() {
final traces = <String, StackTrace?>{
'null': null,
'StackTrace.empty': StackTrace.empty,
'StackTrace.fromString(empty)': StackTrace.fromString(''),
'StackTrace.fromString(whitespace)': StackTrace.fromString(' \n\t'),
'Trace.parse(empty)': Trace.parse(''),
'Chain.parse(empty)': Chain.parse(''),
};
final captureStack = StackTrace.fromString(
'#0 captureSite (package:my_app/capture.dart:10:1)',
);

for (final wrapped in [false, true]) {
for (final entry in traces.entries) {
test(
'${wrapped ? 'wrapped' : 'unwrapped'} ${entry.key} preserves the original Error stack',
() {
late Error error;
try {
_throwOriginalError();
} catch (caught) {
error = caught as Error;
}
expect(error.stackTrace, isNotNull);

var generated = false;
// The test runner's Chain.capture zone appends frames to empty traces.
final result =
Zone.root.run(() => DartExceptionProcessor.processException(
error: wrapped
? PostHogException(
source: error, mechanism: 'FlutterError')
: error,
stackTrace: entry.value,
stackTraceProvider: () {
generated = true;
return captureStack;
},
));
final exception =
(result['\$exception_list'] as List<Map<String, dynamic>>).first;
final stack = exception['stacktrace'] as Map<String, dynamic>?;
final frames = stack?['frames'] as List<Map<String, dynamic>>? ?? [];

expect(
frames.map((frame) => frame['function']),
contains('_throwOriginalError'),
reason: 'generated=$generated; mechanism=${exception['mechanism']}',
);
expect(generated, isFalse);
expect(exception['type'], 'StateError');
expect(exception['mechanism'], containsPair('synthetic', false));
expect(exception['mechanism'], containsPair('handled', !wrapped));
expect(exception['mechanism'],
containsPair('type', wrapped ? 'FlutterError' : 'generic'));
});
}
}

for (final entry in traces.entries) {
test('${entry.key} generates a fallback when no original exists', () {
final result =
Zone.root.run(() => DartExceptionProcessor.processException(
error: Exception('no original stack'),
stackTrace: entry.value,
stackTraceProvider: () => captureStack,
));
final exception =
(result['\$exception_list'] as List<Map<String, dynamic>>).first;
final stack = exception['stacktrace'] as Map<String, dynamic>?;
final frames = stack?['frames'] as List<Map<String, dynamic>>? ?? [];

expect(frames.map((frame) => frame['function']), contains('captureSite'));
expect(exception['mechanism'], containsPair('synthetic', true));
});
}

test('a usable supplied stack takes precedence over the original', () {
try {
_throwOriginalError();
} catch (error) {
final result =
Zone.root.run(() => DartExceptionProcessor.processException(
error: error,
stackTrace: captureStack,
stackTraceProvider: () =>
throw StateError('must use supplied stack'),
));
final exception =
(result['\$exception_list'] as List<Map<String, dynamic>>).first;
final stack = exception['stacktrace'] as Map<String, dynamic>;
final frames = stack['frames'] as List<Map<String, dynamic>>;

expect(frames.map((frame) => frame['function']), ['captureSite']);
expect(exception['mechanism'], containsPair('synthetic', false));
}
});
}
Loading