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 <noreply@anthropic.com>
This commit is contained in:
Tommy Chow 2026-03-08 00:16:13 -08:00
parent f07fe621bc
commit 6ede000c66
21 changed files with 191 additions and 41 deletions

View file

@ -1,4 +1,5 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
/// Common type aliases to reduce repetition /// Common type aliases to reduce repetition
typedef JsonMap = Map<String, dynamic>; typedef JsonMap = Map<String, dynamic>;
@ -157,29 +158,35 @@ abstract class BaseApiClient {
/// Converts DioException to appropriate ApiException subtype /// Converts DioException to appropriate ApiException subtype
ApiException _handleError(DioException error) { ApiException _handleError(DioException error) {
switch (error.type) { final exception = switch (error.type) {
case DioExceptionType.connectionTimeout: DioExceptionType.connectionTimeout ||
case DioExceptionType.sendTimeout: DioExceptionType.sendTimeout ||
case DioExceptionType.receiveTimeout: DioExceptionType.receiveTimeout =>
return TimeoutException(_getTimeoutMessage(error.type)); TimeoutException(_getTimeoutMessage(error.type)),
DioExceptionType.connectionError => NetworkException(
case DioExceptionType.connectionError:
return NetworkException(
'No internet connection. Please check your network.', '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'),
};
// 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}',
); );
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');
} }
return exception;
} }
/// Gets specific timeout message based on timeout type /// Gets specific timeout message based on timeout type

View file

@ -1,5 +1,6 @@
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:frosty/apis/firebase_performance_interceptor.dart';
/// Centralized Dio client configuration with interceptors and error handling /// Centralized Dio client configuration with interceptors and error handling
class DioClient { class DioClient {
@ -55,6 +56,9 @@ class DioClient {
); );
} }
// Trace HTTP requests as Firebase Performance metrics
dio.interceptors.add(FirebasePerformanceInterceptor());
// Add any additional interceptors provided // Add any additional interceptors provided
if (additionalInterceptors != null) { if (additionalInterceptors != null) {
for (final interceptor in additionalInterceptors) { for (final interceptor in additionalInterceptors) {

View file

@ -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,
};
}

View file

@ -3,8 +3,10 @@ import 'dart:convert';
import 'package:advanced_in_app_review/advanced_in_app_review.dart'; import 'package:advanced_in_app_review/advanced_in_app_review.dart';
import 'package:app_links/app_links.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_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter_mobx/flutter_mobx.dart';
@ -77,6 +79,19 @@ void main() async {
// Initialize a settings store from the settings JSON string. // Initialize a settings store from the settings JSON string.
final settingsStore = SettingsStore.fromJson(jsonDecode(userSettings)); 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. // Create a MobX reaction that will save the settings on disk every time they are changed.
autorun((_) => prefs.setString('settings', jsonEncode(settingsStore))); autorun((_) => prefs.setString('settings', jsonEncode(settingsStore)));
@ -108,6 +123,10 @@ void main() async {
dioClient.interceptors.add(UnauthorizedInterceptor(authStore)); dioClient.interceptors.add(UnauthorizedInterceptor(authStore));
await authStore.init(); 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( runApp(
MultiProvider( MultiProvider(
@ -173,6 +192,11 @@ class _MyAppState extends State<MyApp> {
: settingsStore.themeType == ThemeType.light : settingsStore.themeType == ThemeType.light
? ThemeMode.light ? ThemeMode.light
: ThemeMode.dark, : ThemeMode.dark,
navigatorObservers: [
FirebaseAnalyticsObserver(
analytics: FirebaseAnalytics.instance,
),
],
home: widget.firstRun ? const OnboardingIntro() : const Home(), home: widget.firstRun ? const OnboardingIntro() : const Home(),
navigatorKey: navigatorKey, navigatorKey: navigatorKey,
); );
@ -227,6 +251,7 @@ class _MyAppState extends State<MyApp> {
final user = await twitchApi.getUser(userLogin: channelName); final user = await twitchApi.getUser(userLogin: channelName);
final route = MaterialPageRoute( final route = MaterialPageRoute(
settings: const RouteSettings(name: VideoChat.routeName),
builder: (context) => VideoChat( builder: (context) => VideoChat(
userId: user.id, userId: user.id,
userName: user.displayName, userName: user.displayName,

View file

@ -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. /// Creates a widget that shows the video stream (if live) and chat of the given user.
class VideoChat extends StatefulWidget { class VideoChat extends StatefulWidget {
static const routeName = 'VideoChat';
final String userId; final String userId;
final String userName; final String userName;
final String userLogin; final String userLogin;

View file

@ -416,6 +416,7 @@ class _ChatDetailsState extends State<ChatDetails> {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: Settings.routeName),
builder: (context) => builder: (context) =>
Settings(settingsStore: widget.chatStore.settings), Settings(settingsStore: widget.chatStore.settings),
), ),

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:convert'; import 'dart:convert';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:frosty/apis/twitch_api.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: () { onDone: () {
_clearPendingDelayedCallbacks(clearChat: false); _clearPendingDelayedCallbacks(clearChat: false);
debugPrint('7TV events done'); debugPrint('7TV events done');
@ -874,6 +882,8 @@ abstract class ChatStoreBase with Store {
@action @action
Future<void> connectToChat({bool isReconnect = false}) async { Future<void> connectToChat({bool isReconnect = false}) async {
FirebaseCrashlytics.instance.setCustomKey('channel', channelName);
// Fetch assets first so they're available for all messages // Fetch assets first so they're available for all messages
getAssets().catchError( getAssets().catchError(
(e) => debugPrint('Failed to fetch chat assets: $e'), (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 { onDone: () async {
_clearPendingDelayedCallbacks(clearSevenTV: false); _clearPendingDelayedCallbacks(clearSevenTV: false);
@ -924,6 +941,9 @@ abstract class ChatStoreBase with Store {
} }
if (_retries >= _maxRetries) { if (_retries >= _maxRetries) {
FirebaseCrashlytics.instance.log(
'WebSocket reconnection failed after $_maxRetries attempts, channel=$channelName',
);
// Remove the reconnect message before showing final disconnect notice // Remove the reconnect message before showing final disconnect notice
if (_reconnectMessage != null) { if (_reconnectMessage != null) {
final index = _messages.indexOf(_reconnectMessage!); final index = _messages.indexOf(_reconnectMessage!);
@ -949,6 +969,9 @@ abstract class ChatStoreBase with Store {
// Increment the retry count. // Increment the retry count.
_retries++; _retries++;
FirebaseCrashlytics.instance.log(
'WebSocket reconnecting: attempt $_retries/$_maxRetries, channel=$channelName',
);
// Start exponential backoff only after first failed attempt (capped at 8s). // Start exponential backoff only after first failed attempt (capped at 8s).
// First retry is immediate to catch brief network hiccups. // First retry is immediate to catch brief network hiccups.

View file

@ -202,6 +202,7 @@ class StreamInfoBar extends StatelessWidget {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CategoryStreams.routeName),
builder: (context) => builder: (context) =>
CategoryStreams( CategoryStreams(
categoryId: categoryId:
@ -293,6 +294,7 @@ class StreamInfoBar extends StatelessWidget {
onDoubleTap: () => Navigator.push( onDoubleTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CategoryStreams.routeName),
builder: (context) => CategoryStreams( builder: (context) => CategoryStreams(
categoryId: categoryId:
streamInfo?.gameId ?? '', streamInfo?.gameId ?? '',

View file

@ -44,7 +44,10 @@ class _HomeState extends State<Home> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => const ReleaseNotes()), MaterialPageRoute(
settings: const RouteSettings(name: ReleaseNotes.routeName),
builder: (context) => const ReleaseNotes(),
),
).then((_) => prefs.setString('last_shown_version', currentVersion)); ).then((_) => prefs.setString('last_shown_version', currentVersion));
} }
} }
@ -121,6 +124,7 @@ class _HomeState extends State<Home> {
onPressed: () => Navigator.push( onPressed: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: Settings.routeName),
builder: (context) => Settings( builder: (context) => Settings(
settingsStore: context.read<SettingsStore>(), settingsStore: context.read<SettingsStore>(),
), ),

View file

@ -39,6 +39,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: VideoChat.routeName),
builder: (context) => VideoChat( builder: (context) => VideoChat(
userId: channelInfo.broadcasterId, userId: channelInfo.broadcasterId,
userName: channelInfo.broadcasterName, userName: channelInfo.broadcasterName,
@ -120,6 +121,7 @@ class _SearchResultsChannelsState extends State<SearchResultsChannels> {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: VideoChat.routeName),
builder: (context) => VideoChat( builder: (context) => VideoChat(
userId: channel.id, userId: channel.id,
userName: channel.displayName, userName: channel.displayName,

View file

@ -75,6 +75,7 @@ class LargeStreamCard extends StatelessWidget {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: VideoChat.routeName),
builder: (context) => VideoChat( builder: (context) => VideoChat(
userId: streamInfo.userId, userId: streamInfo.userId,
userName: streamInfo.userName, userName: streamInfo.userName,

View file

@ -57,6 +57,7 @@ class OfflineChannelCard extends StatelessWidget {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: VideoChat.routeName),
builder: (context) => VideoChat( builder: (context) => VideoChat(
userId: channelInfo.broadcasterId, userId: channelInfo.broadcasterId,
userName: channelInfo.broadcasterName, userName: channelInfo.broadcasterName,

View file

@ -168,6 +168,7 @@ class StreamCard extends StatelessWidget {
? () => Navigator.push( ? () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CategoryStreams.routeName),
builder: (context) => builder: (context) =>
CategoryStreams(categoryId: streamInfo.gameId), CategoryStreams(categoryId: streamInfo.gameId),
), ),
@ -203,6 +204,7 @@ class StreamCard extends StatelessWidget {
onTap: () => Navigator.push( onTap: () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: VideoChat.routeName),
builder: (context) => VideoChat( builder: (context) => VideoChat(
userId: streamInfo.userId, userId: streamInfo.userId,
userName: streamInfo.userName, userName: streamInfo.userName,

View file

@ -29,6 +29,7 @@ class CategoryCard extends StatelessWidget {
? () => Navigator.push( ? () => Navigator.push(
context, context,
MaterialPageRoute( MaterialPageRoute(
settings: const RouteSettings(name: CategoryStreams.routeName),
builder: (context) => CategoryStreams(categoryId: category.id), builder: (context) => CategoryStreams(categoryId: category.id),
), ),
) )

View file

@ -13,6 +13,8 @@ import 'package:provider/provider.dart';
/// A widget that displays a list of streams under the provided [categoryId]. /// A widget that displays a list of streams under the provided [categoryId].
class CategoryStreams extends StatefulWidget { class CategoryStreams extends StatefulWidget {
static const routeName = 'CategoryStreams';
/// The category id, used for fetching the relevant streams in the [ListStore]. /// The category id, used for fetching the relevant streams in the [ListStore].
final String categoryId; final String categoryId;

View file

@ -65,7 +65,10 @@ class _OtherSettingsState extends State<OtherSettings> {
title: const Text('Release notes'), title: const Text('Release notes'),
onTap: () => Navigator.of( onTap: () => Navigator.of(
context, context,
).push(MaterialPageRoute(builder: (context) => const ReleaseNotes())), ).push(MaterialPageRoute(
settings: const RouteSettings(name: ReleaseNotes.routeName),
builder: (context) => const ReleaseNotes(),
)),
), ),
ListTile( ListTile(
leading: const Icon(Icons.delete_outline_rounded), leading: const Icon(Icons.delete_outline_rounded),

View file

@ -17,6 +17,8 @@ import 'package:simple_icons/simple_icons.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class Settings extends StatelessWidget { class Settings extends StatelessWidget {
static const routeName = 'Settings';
final SettingsStore settingsStore; final SettingsStore settingsStore;
const Settings({super.key, required this.settingsStore}); const Settings({super.key, required this.settingsStore});

View file

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:frosty/apis/base_api_client.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; _error = null;
} catch (e) { } catch (e) {
debugPrint(e.toString()); debugPrint(e.toString());
@ -277,6 +279,8 @@ abstract class AuthBase with Store {
// Set the login status to logged in. // Set the login status to logged in.
if (user.details != null) { if (user.details != null) {
_isLoggedIn = true; _isLoggedIn = true;
FirebaseCrashlytics.instance.setCustomKey('is_logged_in', true);
FirebaseCrashlytics.instance.setUserIdentifier(user.details!.id);
_stopReconnectLoop(); _stopReconnectLoop();
} }
} catch (e) { } catch (e) {
@ -307,6 +311,8 @@ abstract class AuthBase with Store {
// Set the login status to logged out. // Set the login status to logged out.
_isLoggedIn = false; _isLoggedIn = false;
FirebaseCrashlytics.instance.setCustomKey('is_logged_in', false);
FirebaseCrashlytics.instance.setUserIdentifier('');
debugPrint('Successfully logged out'); debugPrint('Successfully logged out');
} catch (e) { } catch (e) {
@ -321,6 +327,9 @@ abstract class AuthBase with Store {
try { try {
_reconnectAttempts++; _reconnectAttempts++;
if (_reconnectAttempts > _maxReconnectAttempts) { if (_reconnectAttempts > _maxReconnectAttempts) {
FirebaseCrashlytics.instance.log(
'Auth reconnection exhausted after $_maxReconnectAttempts attempts',
);
await logout(); await logout();
return; return;
} }

View file

@ -8,6 +8,8 @@ import 'package:provider/provider.dart';
import 'package:url_launcher/url_launcher_string.dart'; import 'package:url_launcher/url_launcher_string.dart';
class ReleaseNotes extends StatefulWidget { class ReleaseNotes extends StatefulWidget {
static const routeName = 'ReleaseNotes';
const ReleaseNotes({super.key}); const ReleaseNotes({super.key});
@override @override

View file

@ -24,6 +24,7 @@ class SettingsTileRoute extends StatelessWidget {
trailing: const Icon(Icons.chevron_right_rounded), trailing: const Icon(Icons.chevron_right_rounded),
onTap: () => Navigator.of(context).push( onTap: () => Navigator.of(context).push(
MaterialPageRoute( MaterialPageRoute(
settings: RouteSettings(name: title),
builder: (context) => useScaffold builder: (context) => useScaffold
? _BlurredSettingsPage(title: title, child: child) ? _BlurredSettingsPage(title: title, child: child)
: child, : child,

View file

@ -1,3 +1,4 @@
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:frosty/apis/bttv_api.dart'; import 'package:frosty/apis/bttv_api.dart';
import 'package:frosty/apis/ffz_api.dart'; import 'package:frosty/apis/ffz_api.dart';
@ -166,10 +167,13 @@ abstract class GlobalAssetsStoreBase with Store {
required bool showFFZEmotes, required bool showFFZEmotes,
required bool showFFZBadges, required bool showFFZBadges,
}) async { }) async {
// Error handler for emotes void onAssetError(dynamic error, StackTrace stack) {
List<Emote> onEmoteError(dynamic error) { debugPrint('GlobalAssetsStore error: $error');
debugPrint('GlobalAssetsStore emote error: $error'); FirebaseCrashlytics.instance.recordError(
return <Emote>[]; error,
stack,
reason: 'Global asset fetch failed',
);
} }
await Future.wait([ await Future.wait([
@ -178,8 +182,9 @@ abstract class GlobalAssetsStoreBase with Store {
twitchApi twitchApi
.getEmotesGlobal() .getEmotesGlobal()
.then((emotes) => _twitchGlobalEmotes = emotes) .then((emotes) => _twitchGlobalEmotes = emotes)
.catchError((e) { .catchError((e, StackTrace stack) {
_twitchGlobalEmotes = onEmoteError(e); onAssetError(e, stack);
_twitchGlobalEmotes = <Emote>[];
return _twitchGlobalEmotes; return _twitchGlobalEmotes;
}), }),
// 7TV global emotes // 7TV global emotes
@ -187,8 +192,9 @@ abstract class GlobalAssetsStoreBase with Store {
sevenTVApi sevenTVApi
.getEmotesGlobal() .getEmotesGlobal()
.then((emotes) => _sevenTVGlobalEmotes = emotes) .then((emotes) => _sevenTVGlobalEmotes = emotes)
.catchError((e) { .catchError((e, StackTrace stack) {
_sevenTVGlobalEmotes = onEmoteError(e); onAssetError(e, stack);
_sevenTVGlobalEmotes = <Emote>[];
return _sevenTVGlobalEmotes; return _sevenTVGlobalEmotes;
}), }),
// BTTV global emotes // BTTV global emotes
@ -196,8 +202,9 @@ abstract class GlobalAssetsStoreBase with Store {
bttvApi bttvApi
.getEmotesGlobal() .getEmotesGlobal()
.then((emotes) => _bttvGlobalEmotes = emotes) .then((emotes) => _bttvGlobalEmotes = emotes)
.catchError((e) { .catchError((e, StackTrace stack) {
_bttvGlobalEmotes = onEmoteError(e); onAssetError(e, stack);
_bttvGlobalEmotes = <Emote>[];
return _bttvGlobalEmotes; return _bttvGlobalEmotes;
}), }),
// FFZ global emotes // FFZ global emotes
@ -205,8 +212,9 @@ abstract class GlobalAssetsStoreBase with Store {
ffzApi ffzApi
.getEmotesGlobal() .getEmotesGlobal()
.then((emotes) => _ffzGlobalEmotes = emotes) .then((emotes) => _ffzGlobalEmotes = emotes)
.catchError((e) { .catchError((e, StackTrace stack) {
_ffzGlobalEmotes = onEmoteError(e); onAssetError(e, stack);
_ffzGlobalEmotes = <Emote>[];
return _ffzGlobalEmotes; return _ffzGlobalEmotes;
}), }),
@ -215,8 +223,8 @@ abstract class GlobalAssetsStoreBase with Store {
twitchApi twitchApi
.getBadgesGlobal() .getBadgesGlobal()
.then((badges) => _twitchGlobalBadges = badges) .then((badges) => _twitchGlobalBadges = badges)
.catchError((e) { .catchError((e, StackTrace stack) {
debugPrint('GlobalAssetsStore badge error: $e'); onAssetError(e, stack);
_twitchGlobalBadges = <String, ChatBadge>{}; _twitchGlobalBadges = <String, ChatBadge>{};
return _twitchGlobalBadges; return _twitchGlobalBadges;
}), }),
@ -224,8 +232,9 @@ abstract class GlobalAssetsStoreBase with Store {
if (showBTTVBadges) if (showBTTVBadges)
bttvApi.getBadges().then((badges) => _bttvBadges = badges).catchError(( bttvApi.getBadges().then((badges) => _bttvBadges = badges).catchError((
e, e,
StackTrace stack,
) { ) {
debugPrint('GlobalAssetsStore badge error: $e'); onAssetError(e, stack);
_bttvBadges = <String, ChatBadge>{}; _bttvBadges = <String, ChatBadge>{};
return _bttvBadges; return _bttvBadges;
}), }),
@ -233,8 +242,9 @@ abstract class GlobalAssetsStoreBase with Store {
if (showFFZBadges) if (showFFZBadges)
ffzApi.getBadges().then((badges) => _ffzBadges = badges).catchError(( ffzApi.getBadges().then((badges) => _ffzBadges = badges).catchError((
e, e,
StackTrace stack,
) { ) {
debugPrint('GlobalAssetsStore badge error: $e'); onAssetError(e, stack);
_ffzBadges = <String, List<ChatBadge>>{}; _ffzBadges = <String, List<ChatBadge>>{};
return _ffzBadges; return _ffzBadges;
}), }),