From 6ede000c66939b39e0b94f494015ce993eef7f69 Mon Sep 17 00:00:00 2001 From: Tommy Chow Date: Sun, 8 Mar 2026 00:16:13 -0800 Subject: [PATCH] improve Firebase integration across Crashlytics, Analytics, and Performance - Fix cold-start privacy bug: apply persisted collection preference on launch - Disable Firebase collection in debug builds to avoid polluting prod data - Add Crashlytics custom keys (is_logged_in, channel), user identifier, and WebSocket reconnection breadcrumbs for actionable crash reports - Record non-fatal errors for server failures, WebSocket errors, and global asset fetch failures (skip timeouts/network errors to avoid noise) - Update is_logged_in key and user identifier on login/logout transitions - Log auth reconnection exhaustion before forced logout - Add FirebaseAnalyticsObserver with named routes for automatic screen tracking - Add static routeName constants to avoid duplicated string literals - Add Firebase Performance Dio interceptor for automatic HTTP request tracing Co-Authored-By: Claude Opus 4.6 --- lib/apis/base_api_client.dart | 49 +++++++++++-------- lib/apis/dio_client.dart | 4 ++ .../firebase_performance_interceptor.dart | 46 +++++++++++++++++ lib/main.dart | 25 ++++++++++ lib/screens/channel/channel.dart | 2 + .../channel/chat/details/chat_details.dart | 1 + .../channel/chat/stores/chat_store.dart | 27 +++++++++- .../channel/video/stream_info_bar.dart | 2 + lib/screens/home/home.dart | 6 ++- .../home/search/search_results_channels.dart | 2 + .../home/stream_list/large_stream_card.dart | 1 + .../stream_list/offline_channel_card.dart | 1 + lib/screens/home/stream_list/stream_card.dart | 2 + .../home/top/categories/category_card.dart | 1 + .../home/top/categories/category_streams.dart | 2 + lib/screens/settings/other_settings.dart | 5 +- lib/screens/settings/settings.dart | 2 + lib/screens/settings/stores/auth_store.dart | 9 ++++ .../settings/widgets/release_notes.dart | 2 + .../settings/widgets/settings_tile_route.dart | 1 + lib/stores/global_assets_store.dart | 42 ++++++++++------ 21 files changed, 191 insertions(+), 41 deletions(-) create mode 100644 lib/apis/firebase_performance_interceptor.dart diff --git a/lib/apis/base_api_client.dart b/lib/apis/base_api_client.dart index 5c8a18e0..f8eb49c4 100644 --- a/lib/apis/base_api_client.dart +++ b/lib/apis/base_api_client.dart @@ -1,4 +1,5 @@ import 'package:dio/dio.dart'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; /// Common type aliases to reduce repetition typedef JsonMap = Map; @@ -157,29 +158,35 @@ abstract class BaseApiClient { /// Converts DioException to appropriate ApiException subtype ApiException _handleError(DioException error) { - switch (error.type) { - case DioExceptionType.connectionTimeout: - case DioExceptionType.sendTimeout: - case DioExceptionType.receiveTimeout: - return TimeoutException(_getTimeoutMessage(error.type)); + final exception = switch (error.type) { + DioExceptionType.connectionTimeout || + DioExceptionType.sendTimeout || + DioExceptionType.receiveTimeout => + TimeoutException(_getTimeoutMessage(error.type)), + DioExceptionType.connectionError => NetworkException( + 'No internet connection. Please check your network.', + ), + DioExceptionType.badResponse => _handleHttpError(error), + DioExceptionType.cancel => ApiException('Request was cancelled'), + DioExceptionType.badCertificate => + ApiException('Security certificate error'), + DioExceptionType.unknown => + ApiException(error.message ?? 'An unexpected error occurred'), + }; - case DioExceptionType.connectionError: - return NetworkException( - 'No internet connection. Please check your network.', - ); - - case DioExceptionType.badResponse: - return _handleHttpError(error); - - case DioExceptionType.cancel: - return ApiException('Request was cancelled'); - - case DioExceptionType.badCertificate: - return ApiException('Security certificate error'); - - case DioExceptionType.unknown: - return ApiException(error.message ?? 'An unexpected error occurred'); + // Record server errors and unexpected failures to Crashlytics. + // Skip timeouts, network errors (normal offline conditions), 401s, and cancellations. + if (exception is ServerException || + error.type == DioExceptionType.unknown || + error.type == DioExceptionType.badCertificate) { + FirebaseCrashlytics.instance.recordError( + exception, + error.stackTrace, + reason: '${error.requestOptions.method} ${error.requestOptions.uri}', + ); } + + return exception; } /// Gets specific timeout message based on timeout type diff --git a/lib/apis/dio_client.dart b/lib/apis/dio_client.dart index 13cd2197..1ba5e538 100644 --- a/lib/apis/dio_client.dart +++ b/lib/apis/dio_client.dart @@ -1,5 +1,6 @@ import 'package:dio/dio.dart'; import 'package:flutter/foundation.dart'; +import 'package:frosty/apis/firebase_performance_interceptor.dart'; /// Centralized Dio client configuration with interceptors and error handling class DioClient { @@ -55,6 +56,9 @@ class DioClient { ); } + // Trace HTTP requests as Firebase Performance metrics + dio.interceptors.add(FirebasePerformanceInterceptor()); + // Add any additional interceptors provided if (additionalInterceptors != null) { for (final interceptor in additionalInterceptors) { diff --git a/lib/apis/firebase_performance_interceptor.dart b/lib/apis/firebase_performance_interceptor.dart new file mode 100644 index 00000000..f1c6712b --- /dev/null +++ b/lib/apis/firebase_performance_interceptor.dart @@ -0,0 +1,46 @@ +import 'package:dio/dio.dart'; +import 'package:firebase_performance/firebase_performance.dart'; + +/// Dio interceptor that traces HTTP requests as Firebase Performance metrics. +class FirebasePerformanceInterceptor extends Interceptor { + static const _metricKey = 'firebase_performance_metric'; + + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + final metric = FirebasePerformance.instance.newHttpMetric( + options.uri.toString(), + _getHttpMethod(options.method), + ); + metric.start(); + options.extra[_metricKey] = metric; + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + _stopMetric(response.requestOptions, response.statusCode); + handler.next(response); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + _stopMetric(err.requestOptions, err.response?.statusCode); + handler.next(err); + } + + void _stopMetric(RequestOptions options, int? statusCode) { + final metric = options.extra.remove(_metricKey) as HttpMetric?; + if (metric == null) return; + if (statusCode != null) metric.httpResponseCode = statusCode; + metric.stop(); + } + + static HttpMethod _getHttpMethod(String method) => switch (method.toUpperCase()) { + 'GET' => HttpMethod.Get, + 'POST' => HttpMethod.Post, + 'PUT' => HttpMethod.Put, + 'DELETE' => HttpMethod.Delete, + 'PATCH' => HttpMethod.Patch, + _ => HttpMethod.Get, + }; +} diff --git a/lib/main.dart b/lib/main.dart index c3462b88..cc428c84 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -3,8 +3,10 @@ import 'dart:convert'; import 'package:advanced_in_app_review/advanced_in_app_review.dart'; import 'package:app_links/app_links.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart'; +import 'package:firebase_performance/firebase_performance.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -77,6 +79,19 @@ void main() async { // Initialize a settings store from the settings JSON string. final settingsStore = SettingsStore.fromJson(jsonDecode(userSettings)); + // Disable Firebase collection in debug builds to avoid polluting production data. + // In release builds, respect the user's preference. + if (kDebugMode) { + FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(false); + FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(false); + FirebasePerformance.instance.setPerformanceCollectionEnabled(false); + } else { + final shareEnabled = settingsStore.shareCrashLogsAndAnalytics; + FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(shareEnabled); + FirebaseAnalytics.instance.setAnalyticsCollectionEnabled(shareEnabled); + FirebasePerformance.instance.setPerformanceCollectionEnabled(shareEnabled); + } + // Create a MobX reaction that will save the settings on disk every time they are changed. autorun((_) => prefs.setString('settings', jsonEncode(settingsStore))); @@ -108,6 +123,10 @@ void main() async { dioClient.interceptors.add(UnauthorizedInterceptor(authStore)); await authStore.init(); + FirebaseCrashlytics.instance.setCustomKey('is_logged_in', authStore.isLoggedIn); + if (authStore.isLoggedIn && authStore.user.details != null) { + FirebaseCrashlytics.instance.setUserIdentifier(authStore.user.details!.id); + } runApp( MultiProvider( @@ -173,6 +192,11 @@ class _MyAppState extends State { : settingsStore.themeType == ThemeType.light ? ThemeMode.light : ThemeMode.dark, + navigatorObservers: [ + FirebaseAnalyticsObserver( + analytics: FirebaseAnalytics.instance, + ), + ], home: widget.firstRun ? const OnboardingIntro() : const Home(), navigatorKey: navigatorKey, ); @@ -227,6 +251,7 @@ class _MyAppState extends State { final user = await twitchApi.getUser(userLogin: channelName); final route = MaterialPageRoute( + settings: const RouteSettings(name: VideoChat.routeName), builder: (context) => VideoChat( userId: user.id, userName: user.displayName, diff --git a/lib/screens/channel/channel.dart b/lib/screens/channel/channel.dart index 5d67d0fc..3d24bb26 100644 --- a/lib/screens/channel/channel.dart +++ b/lib/screens/channel/channel.dart @@ -21,6 +21,8 @@ import 'package:simple_pip_mode/pip_widget.dart'; /// Creates a widget that shows the video stream (if live) and chat of the given user. class VideoChat extends StatefulWidget { + static const routeName = 'VideoChat'; + final String userId; final String userName; final String userLogin; diff --git a/lib/screens/channel/chat/details/chat_details.dart b/lib/screens/channel/chat/details/chat_details.dart index 89c225d4..987e15bf 100644 --- a/lib/screens/channel/chat/details/chat_details.dart +++ b/lib/screens/channel/chat/details/chat_details.dart @@ -416,6 +416,7 @@ class _ChatDetailsState extends State { onTap: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: Settings.routeName), builder: (context) => Settings(settingsStore: widget.chatStore.settings), ), diff --git a/lib/screens/channel/chat/stores/chat_store.dart b/lib/screens/channel/chat/stores/chat_store.dart index d0c020b4..e8b5d607 100644 --- a/lib/screens/channel/chat/stores/chat_store.dart +++ b/lib/screens/channel/chat/stores/chat_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:frosty/apis/twitch_api.dart'; @@ -862,7 +863,14 @@ abstract class ChatStoreBase with Store { ); } }, - onError: (error) => debugPrint('7TV events error: ${error.toString()}'), + onError: (error) { + debugPrint('7TV events error: ${error.toString()}'); + FirebaseCrashlytics.instance.recordError( + error, + StackTrace.current, + reason: '7TV WebSocket error, channel=$channelName', + ); + }, onDone: () { _clearPendingDelayedCallbacks(clearChat: false); debugPrint('7TV events done'); @@ -874,6 +882,8 @@ abstract class ChatStoreBase with Store { @action Future connectToChat({bool isReconnect = false}) async { + FirebaseCrashlytics.instance.setCustomKey('channel', channelName); + // Fetch assets first so they're available for all messages getAssets().catchError( (e) => debugPrint('Failed to fetch chat assets: $e'), @@ -907,7 +917,14 @@ abstract class ChatStoreBase with Store { }); } }, - onError: (error) => debugPrint('Chat error: ${error.toString()}'), + onError: (error) { + debugPrint('Chat error: ${error.toString()}'); + FirebaseCrashlytics.instance.recordError( + error, + StackTrace.current, + reason: 'IRC WebSocket error, channel=$channelName', + ); + }, onDone: () async { _clearPendingDelayedCallbacks(clearSevenTV: false); @@ -924,6 +941,9 @@ abstract class ChatStoreBase with Store { } if (_retries >= _maxRetries) { + FirebaseCrashlytics.instance.log( + 'WebSocket reconnection failed after $_maxRetries attempts, channel=$channelName', + ); // Remove the reconnect message before showing final disconnect notice if (_reconnectMessage != null) { final index = _messages.indexOf(_reconnectMessage!); @@ -949,6 +969,9 @@ abstract class ChatStoreBase with Store { // Increment the retry count. _retries++; + FirebaseCrashlytics.instance.log( + 'WebSocket reconnecting: attempt $_retries/$_maxRetries, channel=$channelName', + ); // Start exponential backoff only after first failed attempt (capped at 8s). // First retry is immediate to catch brief network hiccups. diff --git a/lib/screens/channel/video/stream_info_bar.dart b/lib/screens/channel/video/stream_info_bar.dart index 8673eb3a..580acfd0 100644 --- a/lib/screens/channel/video/stream_info_bar.dart +++ b/lib/screens/channel/video/stream_info_bar.dart @@ -202,6 +202,7 @@ class StreamInfoBar extends StatelessWidget { Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: CategoryStreams.routeName), builder: (context) => CategoryStreams( categoryId: @@ -293,6 +294,7 @@ class StreamInfoBar extends StatelessWidget { onDoubleTap: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: CategoryStreams.routeName), builder: (context) => CategoryStreams( categoryId: streamInfo?.gameId ?? '', diff --git a/lib/screens/home/home.dart b/lib/screens/home/home.dart index 696eca3b..7bc450c7 100644 --- a/lib/screens/home/home.dart +++ b/lib/screens/home/home.dart @@ -44,7 +44,10 @@ class _HomeState extends State { Navigator.push( context, - MaterialPageRoute(builder: (context) => const ReleaseNotes()), + MaterialPageRoute( + settings: const RouteSettings(name: ReleaseNotes.routeName), + builder: (context) => const ReleaseNotes(), + ), ).then((_) => prefs.setString('last_shown_version', currentVersion)); } } @@ -121,6 +124,7 @@ class _HomeState extends State { onPressed: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: Settings.routeName), builder: (context) => Settings( settingsStore: context.read(), ), diff --git a/lib/screens/home/search/search_results_channels.dart b/lib/screens/home/search/search_results_channels.dart index 53461c80..7ae772be 100644 --- a/lib/screens/home/search/search_results_channels.dart +++ b/lib/screens/home/search/search_results_channels.dart @@ -39,6 +39,7 @@ class _SearchResultsChannelsState extends State { Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: VideoChat.routeName), builder: (context) => VideoChat( userId: channelInfo.broadcasterId, userName: channelInfo.broadcasterName, @@ -120,6 +121,7 @@ class _SearchResultsChannelsState extends State { onTap: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: VideoChat.routeName), builder: (context) => VideoChat( userId: channel.id, userName: channel.displayName, diff --git a/lib/screens/home/stream_list/large_stream_card.dart b/lib/screens/home/stream_list/large_stream_card.dart index 5e7c88a5..f7d9f115 100644 --- a/lib/screens/home/stream_list/large_stream_card.dart +++ b/lib/screens/home/stream_list/large_stream_card.dart @@ -75,6 +75,7 @@ class LargeStreamCard extends StatelessWidget { onTap: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: VideoChat.routeName), builder: (context) => VideoChat( userId: streamInfo.userId, userName: streamInfo.userName, diff --git a/lib/screens/home/stream_list/offline_channel_card.dart b/lib/screens/home/stream_list/offline_channel_card.dart index 0c5005d1..0394ad9e 100644 --- a/lib/screens/home/stream_list/offline_channel_card.dart +++ b/lib/screens/home/stream_list/offline_channel_card.dart @@ -57,6 +57,7 @@ class OfflineChannelCard extends StatelessWidget { onTap: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: VideoChat.routeName), builder: (context) => VideoChat( userId: channelInfo.broadcasterId, userName: channelInfo.broadcasterName, diff --git a/lib/screens/home/stream_list/stream_card.dart b/lib/screens/home/stream_list/stream_card.dart index 57c7bac2..6c7a10c5 100644 --- a/lib/screens/home/stream_list/stream_card.dart +++ b/lib/screens/home/stream_list/stream_card.dart @@ -168,6 +168,7 @@ class StreamCard extends StatelessWidget { ? () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: CategoryStreams.routeName), builder: (context) => CategoryStreams(categoryId: streamInfo.gameId), ), @@ -203,6 +204,7 @@ class StreamCard extends StatelessWidget { onTap: () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: VideoChat.routeName), builder: (context) => VideoChat( userId: streamInfo.userId, userName: streamInfo.userName, diff --git a/lib/screens/home/top/categories/category_card.dart b/lib/screens/home/top/categories/category_card.dart index 07a9038e..5845ae47 100644 --- a/lib/screens/home/top/categories/category_card.dart +++ b/lib/screens/home/top/categories/category_card.dart @@ -29,6 +29,7 @@ class CategoryCard extends StatelessWidget { ? () => Navigator.push( context, MaterialPageRoute( + settings: const RouteSettings(name: CategoryStreams.routeName), builder: (context) => CategoryStreams(categoryId: category.id), ), ) diff --git a/lib/screens/home/top/categories/category_streams.dart b/lib/screens/home/top/categories/category_streams.dart index f4753742..99de8693 100644 --- a/lib/screens/home/top/categories/category_streams.dart +++ b/lib/screens/home/top/categories/category_streams.dart @@ -13,6 +13,8 @@ import 'package:provider/provider.dart'; /// A widget that displays a list of streams under the provided [categoryId]. class CategoryStreams extends StatefulWidget { + static const routeName = 'CategoryStreams'; + /// The category id, used for fetching the relevant streams in the [ListStore]. final String categoryId; diff --git a/lib/screens/settings/other_settings.dart b/lib/screens/settings/other_settings.dart index 5d43b60c..0edda66f 100644 --- a/lib/screens/settings/other_settings.dart +++ b/lib/screens/settings/other_settings.dart @@ -65,7 +65,10 @@ class _OtherSettingsState extends State { title: const Text('Release notes'), onTap: () => Navigator.of( context, - ).push(MaterialPageRoute(builder: (context) => const ReleaseNotes())), + ).push(MaterialPageRoute( + settings: const RouteSettings(name: ReleaseNotes.routeName), + builder: (context) => const ReleaseNotes(), + )), ), ListTile( leading: const Icon(Icons.delete_outline_rounded), diff --git a/lib/screens/settings/settings.dart b/lib/screens/settings/settings.dart index f9c009de..1d750d0f 100644 --- a/lib/screens/settings/settings.dart +++ b/lib/screens/settings/settings.dart @@ -17,6 +17,8 @@ import 'package:simple_icons/simple_icons.dart'; import 'package:url_launcher/url_launcher.dart'; class Settings extends StatelessWidget { + static const routeName = 'Settings'; + final SettingsStore settingsStore; const Settings({super.key, required this.settingsStore}); diff --git a/lib/screens/settings/stores/auth_store.dart b/lib/screens/settings/stores/auth_store.dart index 19d66731..84d4a2ba 100644 --- a/lib/screens/settings/stores/auth_store.dart +++ b/lib/screens/settings/stores/auth_store.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:frosty/apis/base_api_client.dart'; @@ -250,6 +251,7 @@ abstract class AuthBase with Store { } } + FirebaseCrashlytics.instance.setCustomKey('is_logged_in', _isLoggedIn); _error = null; } catch (e) { debugPrint(e.toString()); @@ -277,6 +279,8 @@ abstract class AuthBase with Store { // Set the login status to logged in. if (user.details != null) { _isLoggedIn = true; + FirebaseCrashlytics.instance.setCustomKey('is_logged_in', true); + FirebaseCrashlytics.instance.setUserIdentifier(user.details!.id); _stopReconnectLoop(); } } catch (e) { @@ -307,6 +311,8 @@ abstract class AuthBase with Store { // Set the login status to logged out. _isLoggedIn = false; + FirebaseCrashlytics.instance.setCustomKey('is_logged_in', false); + FirebaseCrashlytics.instance.setUserIdentifier(''); debugPrint('Successfully logged out'); } catch (e) { @@ -321,6 +327,9 @@ abstract class AuthBase with Store { try { _reconnectAttempts++; if (_reconnectAttempts > _maxReconnectAttempts) { + FirebaseCrashlytics.instance.log( + 'Auth reconnection exhausted after $_maxReconnectAttempts attempts', + ); await logout(); return; } diff --git a/lib/screens/settings/widgets/release_notes.dart b/lib/screens/settings/widgets/release_notes.dart index 2a1e82fb..f36f4d12 100644 --- a/lib/screens/settings/widgets/release_notes.dart +++ b/lib/screens/settings/widgets/release_notes.dart @@ -8,6 +8,8 @@ import 'package:provider/provider.dart'; import 'package:url_launcher/url_launcher_string.dart'; class ReleaseNotes extends StatefulWidget { + static const routeName = 'ReleaseNotes'; + const ReleaseNotes({super.key}); @override diff --git a/lib/screens/settings/widgets/settings_tile_route.dart b/lib/screens/settings/widgets/settings_tile_route.dart index 4efe3d54..23bb7c74 100644 --- a/lib/screens/settings/widgets/settings_tile_route.dart +++ b/lib/screens/settings/widgets/settings_tile_route.dart @@ -24,6 +24,7 @@ class SettingsTileRoute extends StatelessWidget { trailing: const Icon(Icons.chevron_right_rounded), onTap: () => Navigator.of(context).push( MaterialPageRoute( + settings: RouteSettings(name: title), builder: (context) => useScaffold ? _BlurredSettingsPage(title: title, child: child) : child, diff --git a/lib/stores/global_assets_store.dart b/lib/stores/global_assets_store.dart index 54e585ad..aedf7126 100644 --- a/lib/stores/global_assets_store.dart +++ b/lib/stores/global_assets_store.dart @@ -1,3 +1,4 @@ +import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:flutter/foundation.dart'; import 'package:frosty/apis/bttv_api.dart'; import 'package:frosty/apis/ffz_api.dart'; @@ -166,10 +167,13 @@ abstract class GlobalAssetsStoreBase with Store { required bool showFFZEmotes, required bool showFFZBadges, }) async { - // Error handler for emotes - List onEmoteError(dynamic error) { - debugPrint('GlobalAssetsStore emote error: $error'); - return []; + void onAssetError(dynamic error, StackTrace stack) { + debugPrint('GlobalAssetsStore error: $error'); + FirebaseCrashlytics.instance.recordError( + error, + stack, + reason: 'Global asset fetch failed', + ); } await Future.wait([ @@ -178,8 +182,9 @@ abstract class GlobalAssetsStoreBase with Store { twitchApi .getEmotesGlobal() .then((emotes) => _twitchGlobalEmotes = emotes) - .catchError((e) { - _twitchGlobalEmotes = onEmoteError(e); + .catchError((e, StackTrace stack) { + onAssetError(e, stack); + _twitchGlobalEmotes = []; return _twitchGlobalEmotes; }), // 7TV global emotes @@ -187,8 +192,9 @@ abstract class GlobalAssetsStoreBase with Store { sevenTVApi .getEmotesGlobal() .then((emotes) => _sevenTVGlobalEmotes = emotes) - .catchError((e) { - _sevenTVGlobalEmotes = onEmoteError(e); + .catchError((e, StackTrace stack) { + onAssetError(e, stack); + _sevenTVGlobalEmotes = []; return _sevenTVGlobalEmotes; }), // BTTV global emotes @@ -196,8 +202,9 @@ abstract class GlobalAssetsStoreBase with Store { bttvApi .getEmotesGlobal() .then((emotes) => _bttvGlobalEmotes = emotes) - .catchError((e) { - _bttvGlobalEmotes = onEmoteError(e); + .catchError((e, StackTrace stack) { + onAssetError(e, stack); + _bttvGlobalEmotes = []; return _bttvGlobalEmotes; }), // FFZ global emotes @@ -205,8 +212,9 @@ abstract class GlobalAssetsStoreBase with Store { ffzApi .getEmotesGlobal() .then((emotes) => _ffzGlobalEmotes = emotes) - .catchError((e) { - _ffzGlobalEmotes = onEmoteError(e); + .catchError((e, StackTrace stack) { + onAssetError(e, stack); + _ffzGlobalEmotes = []; return _ffzGlobalEmotes; }), @@ -215,8 +223,8 @@ abstract class GlobalAssetsStoreBase with Store { twitchApi .getBadgesGlobal() .then((badges) => _twitchGlobalBadges = badges) - .catchError((e) { - debugPrint('GlobalAssetsStore badge error: $e'); + .catchError((e, StackTrace stack) { + onAssetError(e, stack); _twitchGlobalBadges = {}; return _twitchGlobalBadges; }), @@ -224,8 +232,9 @@ abstract class GlobalAssetsStoreBase with Store { if (showBTTVBadges) bttvApi.getBadges().then((badges) => _bttvBadges = badges).catchError(( e, + StackTrace stack, ) { - debugPrint('GlobalAssetsStore badge error: $e'); + onAssetError(e, stack); _bttvBadges = {}; return _bttvBadges; }), @@ -233,8 +242,9 @@ abstract class GlobalAssetsStoreBase with Store { if (showFFZBadges) ffzApi.getBadges().then((badges) => _ffzBadges = badges).catchError(( e, + StackTrace stack, ) { - debugPrint('GlobalAssetsStore badge error: $e'); + onAssetError(e, stack); _ffzBadges = >{}; return _ffzBadges; }),