Skip to content

Commit a1b9bd0

Browse files
committed
fix(crash): stop reporting transient google_fonts failures as fatal
google_fonts downloads fonts over HTTP on first use. When the device is offline or the font CDN fails, the load throws an uncaught async error that PlatformDispatcher.onError recorded to Crashlytics with fatal: true. The app keeps running with a fallback font, so this is a transient, non-fatal condition — reporting it as fatal distorts the crash-free metric. Classify google_fonts font-fetch failures (by exception message and by google_fonts stack frames) and record them as non-fatal in both the Flutter and async error handlers. https://claude.ai/code/session_0135nVBYGpwkaQvG13XyS66H
1 parent e9de883 commit a1b9bd0

2 files changed

Lines changed: 60 additions & 3 deletions

File tree

lib/main.dart

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,24 @@ void main() async {
3030
options: DefaultFirebaseOptions.currentPlatform,
3131
);
3232

33-
// Pass all uncaught Flutter errors to Crashlytics
34-
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
33+
// Pass all uncaught Flutter errors to Crashlytics. Transient google_fonts
34+
// font-fetch failures are downgraded to non-fatal (see
35+
// isTransientFontLoadError).
36+
FlutterError.onError = (details) {
37+
if (isTransientFontLoadError(details.exception, details.stack)) {
38+
FirebaseCrashlytics.instance.recordFlutterError(details);
39+
} else {
40+
FirebaseCrashlytics.instance.recordFlutterFatalError(details);
41+
}
42+
};
3543

3644
// Pass all uncaught asynchronous errors to Crashlytics
3745
PlatformDispatcher.instance.onError = (error, stack) {
38-
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
46+
FirebaseCrashlytics.instance.recordError(
47+
error,
48+
stack,
49+
fatal: !isTransientFontLoadError(error, stack),
50+
);
3951
return true;
4052
};
4153

@@ -49,6 +61,18 @@ void main() async {
4961
runApp(const BeerFestivalApp());
5062
}
5163

64+
/// Whether [error] originates from `google_fonts` runtime font fetching.
65+
///
66+
/// google_fonts downloads fonts over HTTP on first use. When the device is
67+
/// offline or the font CDN fails, the load throws an uncaught async error.
68+
/// The app keeps running with a fallback font, so such failures are transient
69+
/// and non-fatal — they must not be recorded to Crashlytics as fatal crashes,
70+
/// which would otherwise distort the crash-free metric.
71+
bool isTransientFontLoadError(Object error, StackTrace? stack) {
72+
if (error.toString().contains('Failed to load font')) return true;
73+
return stack != null && stack.toString().contains('google_fonts');
74+
}
75+
5276

5377
class BeerFestivalApp extends StatelessWidget {
5478
const BeerFestivalApp({super.key});

test/main_test.dart

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -397,4 +397,37 @@ void main() {
397397
expect(find.text('Drink Detail'), findsOneWidget);
398398
});
399399
});
400+
401+
group('isTransientFontLoadError', () {
402+
test('detects google_fonts HTTP fetch failure by message', () {
403+
final error = Exception(
404+
'Failed to load font with url: https://fonts.gstatic.com/s/a/abc.ttf',
405+
);
406+
expect(isTransientFontLoadError(error, StackTrace.empty), isTrue);
407+
});
408+
409+
test('detects font load failure by google_fonts stack frames', () {
410+
// A network-level exception whose message gives no hint, but whose
411+
// stack trace runs through the google_fonts package.
412+
final stack = StackTrace.fromString(
413+
'#0 _httpFetchFontAndSaveToDevice (package:google_fonts/src/google_fonts_base.dart:288)\n'
414+
'#1 loadFontIfNecessary (package:google_fonts/src/google_fonts_base.dart:175)',
415+
);
416+
expect(
417+
isTransientFontLoadError(Exception('connection refused'), stack),
418+
isTrue,
419+
);
420+
});
421+
422+
test('does not flag unrelated application errors as font errors', () {
423+
final stack = StackTrace.fromString(
424+
'#0 BeerProvider.loadDrinks (package:cambridge_beer_festival/providers/beer_provider.dart:270)',
425+
);
426+
expect(
427+
isTransientFontLoadError(Exception('Something went wrong'), stack),
428+
isFalse,
429+
);
430+
expect(isTransientFontLoadError(StateError('bad state'), null), isFalse);
431+
});
432+
});
400433
}

0 commit comments

Comments
 (0)