diff --git a/packages/uni_app/lib/controller/background_workers/background_callback.dart b/packages/uni_app/lib/controller/background_workers/background_callback.dart index f23fcefd51..d2dfb04439 100644 --- a/packages/uni_app/lib/controller/background_workers/background_callback.dart +++ b/packages/uni_app/lib/controller/background_workers/background_callback.dart @@ -1,4 +1,5 @@ import 'package:logger/logger.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/background_workers/notifications.dart'; import 'package:workmanager/workmanager.dart'; @@ -17,6 +18,7 @@ const taskMap = { // when the app is completely terminated Future workerStartCallback() async { Workmanager().executeTask((taskName, inputData) async { + final transaction = Sentry.startTransaction(taskName, 'background_worker'); try { Logger().d('''[$taskName]: Start executing job...'''); @@ -31,18 +33,24 @@ Future workerStartCallback() async { await value.$1(); } }); + transaction.status = const SpanStatus.ok(); return true; } // try to keep the usage of this function BELOW +-30 seconds // to not be punished by the scheduler in future runs. await taskMap[taskName]!.$1(); + transaction.status = const SpanStatus.ok(); } catch (err, st) { + transaction.status = const SpanStatus.internalError(); + await Sentry.captureException(err, stackTrace: st); Logger().e( 'Error while running $taskName job:', error: err, stackTrace: st, ); return false; + } finally { + await transaction.finish(); } return true; }); diff --git a/packages/uni_app/lib/controller/background_workers/notifications.dart b/packages/uni_app/lib/controller/background_workers/notifications.dart index ded650e1ba..8f52a3270d 100644 --- a/packages/uni_app/lib/controller/background_workers/notifications.dart +++ b/packages/uni_app/lib/controller/background_workers/notifications.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:logger/logger.dart'; import 'package:permission_handler/permission_handler.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uni/controller/background_workers/notifications/tuition_notification.dart'; import 'package:uni/controller/local_storage/notification_timeout_storage.dart'; @@ -200,6 +201,18 @@ class NotificationManager { _localNotificationsPlugin, ); } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'notifications') + ..setTag('action', 'display_notification') + ..setContexts('notification', {'id': notification.uniqueID}); + }, + ), + ); Logger().e( 'Error while checking notification ${notification.uniqueID}', error: err, diff --git a/packages/uni_app/lib/controller/fetchers/academics/faculties_fetcher.dart b/packages/uni_app/lib/controller/fetchers/academics/faculties_fetcher.dart index c08a98485d..f20fc8f117 100644 --- a/packages/uni_app/lib/controller/fetchers/academics/faculties_fetcher.dart +++ b/packages/uni_app/lib/controller/fetchers/academics/faculties_fetcher.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:html/parser.dart'; import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/http/client/cookie.dart'; import 'package:uni/session/flows/base/session.dart'; @@ -15,26 +18,48 @@ Future> getStudentFaculties( ).replace(queryParameters: {'pct_codigo': session.username}), ); - final document = parse(response.body); + try { + final document = parse(response.body); - final facultiesList = document - .querySelectorAll('#conteudoinner>ul a') - .map((e) => e.text); + final facultiesList = document + .querySelectorAll('#conteudoinner>ul a') + .map((e) => e.text); - if (facultiesList.isEmpty) { - // The user is enrolled in a single faculty, - // and the selection page is skipped. - // We can extract the faculty from any anchor. - final singleFaculty = document.querySelector('a')!.attributes['href']!; - final uri = Uri.parse(singleFaculty); - final faculty = uri.pathSegments[0]; - return [faculty.toLowerCase()]; - } + if (facultiesList.isEmpty) { + final anchor = document.querySelector('a'); + if (anchor == null) { + throw Exception('No anchor found in page to extract single faculty.'); + } + final singleFaculty = anchor.attributes['href']!; + final uri = Uri.parse(singleFaculty); + final faculty = uri.pathSegments[0]; + return [faculty.toLowerCase()]; + } - // We extract the faculties from the list. - // An example list is (201906166 (FEUP), 201906166 (FCUP)). - final regex = RegExp(r'.*\(([A-Z]+)\)'); - return facultiesList - .map((e) => regex.firstMatch(e)!.group(1)!.toLowerCase()) - .toList(); + final regex = RegExp(r'.*\(([A-Z]+)\)'); + return facultiesList.map((e) { + final match = regex.firstMatch(e); + if (match == null) { + throw Exception('Could not match faculty regex on string: "$e"'); + } + return match.group(1)!.toLowerCase(); + }).toList(); + } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'faculties_fetcher') + ..setTag('action', 'parse_faculties'); + + final text = response.body.replaceAll(RegExp(r'\s+'), ' ').trim(); + final snippet = text.length > 300 ? text.substring(0, 300) : text; + s.setContexts('response_snippet', snippet); + }, + ), + ); + throw Exception('Failed to parse faculties from response'); + } } diff --git a/packages/uni_app/lib/controller/local_storage/database/database.dart b/packages/uni_app/lib/controller/local_storage/database/database.dart index 51ff9249f5..e654b157e6 100644 --- a/packages/uni_app/lib/controller/local_storage/database/database.dart +++ b/packages/uni_app/lib/controller/local_storage/database/database.dart @@ -1,5 +1,6 @@ import 'package:logger/logger.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/model/entities/calendar_event.dart'; import 'package:uni/model/entities/course.dart'; @@ -108,12 +109,22 @@ class Database { final storePath = await _getDatabasePath(); try { _store = await openStore(directory: storePath); - } catch (err) { + } catch (err, st) { // TODO(thePeras): Better error handling if (err.toString().contains('ObjectBoxException')) { + await Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'database') + ..setTag('action', 'database_fallback_delete'); + }, + ); await remove(); _store = await openStore(directory: storePath); } else { + await Sentry.captureException(err, stackTrace: st); Logger().e(err); } } finally { diff --git a/packages/uni_app/lib/controller/local_storage/notification_timeout_storage.dart b/packages/uni_app/lib/controller/local_storage/notification_timeout_storage.dart index b3a3095337..8033c32fc0 100644 --- a/packages/uni_app/lib/controller/local_storage/notification_timeout_storage.dart +++ b/packages/uni_app/lib/controller/local_storage/notification_timeout_storage.dart @@ -1,7 +1,9 @@ +import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; class NotificationTimeoutStorage { NotificationTimeoutStorage._create(); @@ -21,7 +23,18 @@ class NotificationTimeoutStorage { Map _readContentsFile(File file) { try { return jsonDecode(file.readAsStringSync()) as Map; - } on FormatException catch (_) { + } on FormatException catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'local_storage') + ..setTag('action', 'decode_notification_timeout'); + }, + ), + ); return {}; } } diff --git a/packages/uni_app/lib/controller/local_storage/preferences_controller.dart b/packages/uni_app/lib/controller/local_storage/preferences_controller.dart index ad1d45f77b..8ab9ad55ae 100644 --- a/packages/uni_app/lib/controller/local_storage/preferences_controller.dart +++ b/packages/uni_app/lib/controller/local_storage/preferences_controller.dart @@ -4,6 +4,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:uni/model/entities/app_locale.dart'; import 'package:uni/model/entities/exam.dart'; @@ -114,7 +115,18 @@ class PreferencesController { try { value = await _secureStorage.read(key: _userSession); - } catch (err) { + } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'secure_storage') + ..setTag('action', 'delete_all_fallback'); + }, + ), + ); await _secureStorage.deleteAll(); return null; } diff --git a/packages/uni_app/lib/controller/networking/network_router.dart b/packages/uni_app/lib/controller/networking/network_router.dart index 3a78cbd506..ceb94e4816 100644 --- a/packages/uni_app/lib/controller/networking/network_router.dart +++ b/packages/uni_app/lib/controller/networking/network_router.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/local_storage/preferences_controller.dart'; import 'package:uni/http/client/authenticated.dart'; import 'package:uni/http/client/timeout.dart'; @@ -63,7 +64,7 @@ class NetworkRouter { authenticationController ?? AuthenticationController(session); final client = AuthenticatedClient( - TimeoutClient(httpClient ?? http.Client(), timeout: _requestTimeout), + TimeoutClient(httpClient ?? SentryHttpClient(), timeout: _requestTimeout), controller: controller, ); diff --git a/packages/uni_app/lib/controller/parsers/restaurant/parser_restaurants.dart b/packages/uni_app/lib/controller/parsers/restaurant/parser_restaurants.dart index 9f77b74a7a..12523f3a2f 100644 --- a/packages/uni_app/lib/controller/parsers/restaurant/parser_restaurants.dart +++ b/packages/uni_app/lib/controller/parsers/restaurant/parser_restaurants.dart @@ -1,91 +1,114 @@ +import 'dart:async'; import 'package:html/parser.dart'; import 'package:http/http.dart'; import 'package:intl/intl.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/model/entities/meal.dart'; import 'package:uni/model/entities/restaurant.dart'; import 'package:uni/model/utils/day_of_week.dart'; /// Reads restaurants's menu from /feup/pt/CANTINA.EMENTASHOW List getRestaurantsFromHtml(Response response) { - final document = parse(response.body); + try { + final document = parse(response.body); - // Get restaurant reference number and name - final restaurantsHtml = document.querySelectorAll('#conteudoinner ul li > a'); + // Get restaurant reference number and name + final restaurantsHtml = document.querySelectorAll( + '#conteudoinner ul li > a', + ); - final restaurantsTuple = restaurantsHtml.map((restaurantHtml) { - final name = restaurantHtml.text; - final ref = restaurantHtml.attributes['href']?.replaceAll('#', ''); - return (ref ?? '', name); - }).toList(); + final restaurantsTuple = restaurantsHtml.map((restaurantHtml) { + final name = restaurantHtml.text; + final ref = restaurantHtml.attributes['href']?.replaceAll('#', ''); + return (ref ?? '', name); + }).toList(); - // Get restaurant meals and create the Restaurant class - final restaurants = restaurantsTuple.map((restaurantTuple) { - final meals = []; + // Get restaurant meals and create the Restaurant class + final restaurants = restaurantsTuple.map((restaurantTuple) { + final meals = []; - final referenceA = document.querySelector( - 'a[name="${restaurantTuple.$1}"]', - ); - var next = referenceA?.nextElementSibling; + final referenceA = document.querySelector( + 'a[name="${restaurantTuple.$1}"]', + ); + var next = referenceA?.nextElementSibling; - final format = DateFormat('d-M-y'); - while (next != null && next.attributes['name'] == null) { - next = next.nextElementSibling; - if (next!.classes.contains('dados')) { - // It's the menu table - final rows = next.querySelectorAll('tr'); - // Check if is empty - if (rows.length <= 1) { - break; - } + final format = DateFormat('d-M-y'); + while (next != null && next.attributes['name'] == null) { + next = next.nextElementSibling; + if (next!.classes.contains('dados')) { + // It's the menu table + final rows = next.querySelectorAll('tr'); + // Check if is empty + if (rows.length <= 1) { + break; + } - // Read rows, first row is ignored because it's the header - rows.getRange(1, rows.length).forEach((row) { - DayOfWeek? dayOfWeek; - String? type; - DateTime? date; - final columns = row.querySelectorAll('td'); + // Read rows, first row is ignored because it's the header + rows.getRange(1, rows.length).forEach((row) { + DayOfWeek? dayOfWeek; + String? type; + DateTime? date; + final columns = row.querySelectorAll('td'); - for (final column in columns) { - final value = column.text; - final header = column.attributes['headers']; - if (header == 'Data') { - final d = parseDayOfWeek(value); - if (d == null) { - // It's a date - date = format.parseUtc(value); + for (final column in columns) { + final value = column.text; + final header = column.attributes['headers']; + if (header == 'Data') { + final d = parseDayOfWeek(value); + if (d == null) { + // It's a date + date = format.parseUtc(value); + } else { + dayOfWeek = d; + } } else { - dayOfWeek = d; + type = document.querySelector('#$header')?.text; + final meal = Meal( + type ?? '', + value, + value, + date!, + dbDayOfWeek: dayOfWeek!.index, + ); + meals.add(meal); } - } else { - type = document.querySelector('#$header')?.text; - final meal = Meal( - type ?? '', - value, - value, - date!, - dbDayOfWeek: dayOfWeek!.index, - ); - meals.add(meal); } - } - }); - break; + }); + break; + } } - } - return Restaurant( - null, - null, - null, - restaurantTuple.$2, - restaurantTuple.$2, - restaurantTuple.$1, - 2, // Hardcoded to Asprela Campus - '', - [], - '', - meals: meals, + return Restaurant( + null, + null, + null, + restaurantTuple.$2, + restaurantTuple.$2, + restaurantTuple.$1, + 2, // Hardcoded to Asprela Campus + '', + [], + '', + meals: meals, + ); + }).toList(); + return restaurants; + } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'parser_restaurants') + ..setTag('action', 'parse_sigarra_restaurants'); + + final text = response.body.replaceAll(RegExp(r'\s+'), ' ').trim(); + final snippet = text.length > 300 ? text.substring(0, 300) : text; + s.setContexts('response_snippet', snippet); + }, + ), ); - }).toList(); - return restaurants; + throw Exception('Failed to parse restaurants from HTML'); + } } diff --git a/packages/uni_app/lib/main.dart b/packages/uni_app/lib/main.dart index 95b236fb28..30263abc00 100644 --- a/packages/uni_app/lib/main.dart +++ b/packages/uni_app/lib/main.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'dart:io'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; @@ -52,10 +54,6 @@ import 'package:workmanager/workmanager.dart'; import 'controller/local_storage/database/database.dart'; -SentryEvent? beforeSend(SentryEvent event) { - return event.level == SentryLevel.info ? event : null; -} - Future firstRoute() async { final savedSession = await PreferencesController.getSavedSession(); @@ -79,15 +77,20 @@ Future main() async { // Initialize WorkManager for background tasks await Workmanager().initialize(workerStartCallback); - // NoSQL database initialization - await Database().init(); - // Read environment, which may include app tokens await dotenv.load(fileName: 'assets/env/.env', isOptional: true).onError(( error, stackTrace, ) { - Sentry.captureException(error, stackTrace: stackTrace); + Sentry.captureException( + error, + stackTrace: stackTrace, + withScope: (s) { + s + ..setTag('feature', 'env_loader') + ..setTag('action', 'load_dotenv'); + }, + ); Logger().e( 'Error loading .env file: $error', error: error, @@ -115,10 +118,26 @@ Future main() async { await SentryFlutter.init( (options) { - options.dsn = - 'https://a2661645df1c4992b24161010c5e0ecb@o553498.ingest.sentry.io/5680848'; + options + ..dsn = 'https://a2661645df1c4992b24161010c5e0ecb@o553498.ingest.sentry.io/5680848' + ..tracesSampleRate = 0.1 + ..sendDefaultPii = false + ..environment = kReleaseMode ? 'production' : 'debug' + ..beforeSend = (event, hint) { + final throwable = event.throwable; + if (throwable is SocketException || + throwable is OSError || + throwable is TimeoutException || + throwable is HttpException) { + return null; + } + return event; + }; }, - appRunner: () { + appRunner: () async { + // NoSQL database initialization + await Database().init(); + runApp( ProviderScope( overrides: [ @@ -158,6 +177,8 @@ class ApplicationState extends ConsumerState { void initState() { super.initState(); + navigatorObservers.add(SentryNavigatorObserver()); + final plausible = ref.read(plausibleProvider); if (plausible != null) { navigatorObservers.add(PlausibleNavigatorObserver(plausible)); diff --git a/packages/uni_app/lib/model/providers/plausible/plausible_provider.dart b/packages/uni_app/lib/model/providers/plausible/plausible_provider.dart index f68c945e3e..7158e5ee87 100644 --- a/packages/uni_app/lib/model/providers/plausible/plausible_provider.dart +++ b/packages/uni_app/lib/model/providers/plausible/plausible_provider.dart @@ -49,7 +49,17 @@ class _PlausibleProviderState extends ConsumerState { .then((_) => _updateConnectivityState()) .then((_) => _updateUsageStatsState()) .onError((error, stackTrace) { - unawaited(Sentry.captureException(error, stackTrace: stackTrace)); + unawaited( + Sentry.captureException( + error, + stackTrace: stackTrace, + withScope: (s) { + s + ..setTag('feature', 'plausible') + ..setTag('action', 'startListeners'); + }, + ), + ); Logger().e( 'Error initializing plausible: $error', stackTrace: stackTrace, diff --git a/packages/uni_app/lib/model/providers/riverpod/cached_async_notifier.dart b/packages/uni_app/lib/model/providers/riverpod/cached_async_notifier.dart index 45827de6c0..aaa38a3153 100644 --- a/packages/uni_app/lib/model/providers/riverpod/cached_async_notifier.dart +++ b/packages/uni_app/lib/model/providers/riverpod/cached_async_notifier.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/local_storage/preferences_controller.dart'; abstract class CachedAsyncNotifier extends AsyncNotifier { @@ -54,6 +57,7 @@ abstract class CachedAsyncNotifier extends AsyncNotifier { } state = AsyncError(error, stackTrace ?? StackTrace.current); + unawaited(Sentry.captureException(error, stackTrace: stackTrace)); Logger().e( 'Error in $runtimeType: $error', error: error, @@ -86,10 +90,15 @@ abstract class CachedAsyncNotifier extends AsyncNotifier { ); Logger().d('Loading $runtimeType from storage...'); + final span = Sentry.getSpan()?.startChild( + 'provider_fetch_local', + description: runtimeType.toString(), + ); final localData = await _safeExecute( loadFromStorage, updateTimestamp: false, ); + await span?.finish(status: const SpanStatus.ok()); if (localData != null) { Logger().d('✅ Loaded $runtimeType from storage!'); @@ -107,9 +116,14 @@ abstract class CachedAsyncNotifier extends AsyncNotifier { Future refreshRemote() async { Logger().d('Refreshing $runtimeType from remote...'); + final span = Sentry.getSpan()?.startChild( + 'provider_fetch_remote', + description: runtimeType.toString(), + ); try { state = const AsyncLoading(); final result = await loadFromRemote(); + await span?.finish(status: const SpanStatus.ok()); if (!ref.mounted) { return result; diff --git a/packages/uni_app/lib/model/providers/riverpod/faculty_locations_provider.dart b/packages/uni_app/lib/model/providers/riverpod/faculty_locations_provider.dart index 04c13ec412..94c011d0d9 100644 --- a/packages/uni_app/lib/model/providers/riverpod/faculty_locations_provider.dart +++ b/packages/uni_app/lib/model/providers/riverpod/faculty_locations_provider.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/legacy.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/fetchers/campus/locations/location_fetcher_asset.dart'; import 'package:uni/controller/fetchers/campus/locations/location_fetcher_osm.dart'; import 'package:uni/model/entities/faculty_config.dart'; @@ -44,7 +47,18 @@ class FacultyLocationsNotifier if (osmData.isNotEmpty) { return osmData; } - } catch (err) { + } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'faculty_locations_provider') + ..setTag('action', 'fetch_osm'); + }, + ), + ); // not fetching from cache yet, ignore error and fall back to asset. } return LocationFetcherAsset(_faculty).getLocations(); @@ -71,7 +85,18 @@ class IndoorFloorPlansNotifier Future> loadFromRemote() async { try { return await ref.read(_osmFetcherProvider).getIndoorFloorPlans(); - } catch (err) { + } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'indoor_floor_plans_provider') + ..setTag('action', 'fetch_osm'); + }, + ), + ); return loadFromStorage(); } } diff --git a/packages/uni_app/lib/model/providers/riverpod/lecture_provider.dart b/packages/uni_app/lib/model/providers/riverpod/lecture_provider.dart index cf3d7ac341..e377b79f43 100644 --- a/packages/uni_app/lib/model/providers/riverpod/lecture_provider.dart +++ b/packages/uni_app/lib/model/providers/riverpod/lecture_provider.dart @@ -1,5 +1,8 @@ +import 'dart:async'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/fetchers/academics/schedule/schedule_fetcher_new_api.dart'; import 'package:uni/controller/local_storage/database/database.dart'; import 'package:uni/model/entities/lecture.dart'; @@ -33,6 +36,17 @@ class LectureNotifier extends CachedAsyncNotifier> { try { await Database().saveLectures(lectures); } catch (err, st) { + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'lecture_provider') + ..setTag('action', 'save_lectures'); + }, + ), + ); Logger().e( 'Failed to save lectures to local database', error: err, diff --git a/packages/uni_app/lib/session/authentication_controller.dart b/packages/uni_app/lib/session/authentication_controller.dart index 629f6d16ca..54a389fe01 100644 --- a/packages/uni_app/lib/session/authentication_controller.dart +++ b/packages/uni_app/lib/session/authentication_controller.dart @@ -112,7 +112,17 @@ class AuthenticationController { // Report the exception as it will not be thrown when // awaiting a snapshot. Logger().e('Failed to reauthenticate', error: err, stackTrace: st); - unawaited(Sentry.captureException(err, stackTrace: st)); + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (scope) { + scope + ..setTag('feature', 'authentication') + ..setTag('action', 'reauthenticate'); + }, + ), + ); return; } diff --git a/packages/uni_app/lib/session/flows/credentials/request.dart b/packages/uni_app/lib/session/flows/credentials/request.dart index bf4c76b782..b928b28032 100644 --- a/packages/uni_app/lib/session/flows/credentials/request.dart +++ b/packages/uni_app/lib/session/flows/credentials/request.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/controller/fetchers/academics/faculties_fetcher.dart'; import 'package:uni/http/client/cookie.dart'; import 'package:uni/session/exception.dart'; @@ -26,7 +27,7 @@ class CredentialsSessionRequest extends SessionRequest { @override Future perform([http.Client? httpClient]) async { - final client = httpClient ?? http.Client(); + final client = httpClient ?? SentryHttpClient(); // We need to login to fetch the username and faculties, // so perform a temporary login. diff --git a/packages/uni_app/lib/session/flows/federated/client.dart b/packages/uni_app/lib/session/flows/federated/client.dart index d8d2998b2b..0d4c384b82 100644 --- a/packages/uni_app/lib/session/flows/federated/client.dart +++ b/packages/uni_app/lib/session/flows/federated/client.dart @@ -1,9 +1,13 @@ import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:uni/http/client/timeout.dart'; class FederatedDefaultClient extends http.BaseClient { FederatedDefaultClient() - : inner = TimeoutClient(http.Client(), timeout: const Duration(seconds: 5)); + : inner = TimeoutClient( + SentryHttpClient(), + timeout: const Duration(seconds: 5), + ); final http.Client inner; diff --git a/packages/uni_app/lib/session/flows/federated/initiator.dart b/packages/uni_app/lib/session/flows/federated/initiator.dart index 11896d702f..7519d4dbd9 100644 --- a/packages/uni_app/lib/session/flows/federated/initiator.dart +++ b/packages/uni_app/lib/session/flows/federated/initiator.dart @@ -24,7 +24,17 @@ class FederatedSessionInitiator extends SessionInitiator { required AuthenticationException onError, }) { T reportExceptionAndFail(E error, StackTrace st) { - unawaited(Sentry.captureException(error, stackTrace: st)); + unawaited( + Sentry.captureException( + error, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'federated_login') + ..setTag('action', 'openid_exception'); + }, + ), + ); throw onError; } diff --git a/packages/uni_app/lib/session/flows/federated/request.dart b/packages/uni_app/lib/session/flows/federated/request.dart index 09cccfdfb5..1c0014cb43 100644 --- a/packages/uni_app/lib/session/flows/federated/request.dart +++ b/packages/uni_app/lib/session/flows/federated/request.dart @@ -82,7 +82,17 @@ class FederatedSessionRequest extends SessionRequest { E error, StackTrace st, ) { - unawaited(Sentry.captureException(error, stackTrace: st)); + unawaited( + Sentry.captureException( + error, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'federated_login') + ..setTag('action', 'token_request_exception'); + }, + ), + ); return const TokenFailedResponse(); } diff --git a/packages/uni_app/lib/sigarra/endpoints/html/authentication/login/login.dart b/packages/uni_app/lib/sigarra/endpoints/html/authentication/login/login.dart index 6194f7f37a..f08b74c250 100644 --- a/packages/uni_app/lib/sigarra/endpoints/html/authentication/login/login.dart +++ b/packages/uni_app/lib/sigarra/endpoints/html/authentication/login/login.dart @@ -70,12 +70,21 @@ class Login extends Endpoint { throw Exception('Could not find failure reason'); } catch (err, st) { unawaited( - Sentry.captureEvent( - SentryEvent( - throwable: err, - request: SentryRequest(data: document.outerHtml), - ), + Sentry.captureException( + err, stackTrace: st, + withScope: (scope) { + scope + ..setTag('feature', 'login') + ..setTag('action', 'parse_failure_reason'); + final text = document.body?.text + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + final snippet = text != null && text.length > 200 + ? text.substring(0, 200) + : text; + scope.setContexts('body_snippet', snippet); + }, ), ); diff --git a/packages/uni_app/lib/sigarra/options.dart b/packages/uni_app/lib/sigarra/options.dart index 74cbfbf118..ea9d341e60 100644 --- a/packages/uni_app/lib/sigarra/options.dart +++ b/packages/uni_app/lib/sigarra/options.dart @@ -1,7 +1,9 @@ import 'package:http/http.dart' as http; +import 'package:sentry_flutter/sentry_flutter.dart'; abstract class BaseRequestOptions { - BaseRequestOptions({http.Client? client}) : client = client ?? http.Client(); + BaseRequestOptions({http.Client? client}) + : client = client ?? SentryHttpClient(); final http.Client client; diff --git a/packages/uni_app/lib/view/bug_report/bug_report.dart b/packages/uni_app/lib/view/bug_report/bug_report.dart index a48f97371e..2b3e9b0993 100644 --- a/packages/uni_app/lib/view/bug_report/bug_report.dart +++ b/packages/uni_app/lib/view/bug_report/bug_report.dart @@ -410,7 +410,15 @@ class BugReportPageViewState extends SecondaryPageViewState { await ToastMessage.success(context, S.of(context).success); } } catch (err, st) { - await Sentry.captureException(err, stackTrace: st); + await Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'bug_report') + ..setTag('action', 'submit'); + }, + ); Logger().e('Error while posting bug report:$err'); if (mounted) { await ToastMessage.error(context, S.of(context).sent_error); diff --git a/packages/uni_app/lib/view/login/login.dart b/packages/uni_app/lib/view/login/login.dart index 8733ba2d34..7a00b59557 100644 --- a/packages/uni_app/lib/view/login/login.dart +++ b/packages/uni_app/lib/view/login/login.dart @@ -132,7 +132,17 @@ class LoginPageViewState extends ConsumerState ); default: Logger().e(err, stackTrace: st); - unawaited(Sentry.captureException(err, stackTrace: st)); + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'login') + ..setTag('action', 'credentials_login'); + }, + ), + ); unawaited( ToastMessage.error(context, S.of(context).failed_login), ); @@ -140,7 +150,17 @@ class LoginPageViewState extends ConsumerState } else { // Not mounted: log and capture the error but avoid UI calls Logger().e(err, stackTrace: st); - unawaited(Sentry.captureException(err, stackTrace: st)); + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'login') + ..setTag('action', 'credentials_login'); + }, + ), + ); } } // Handles other unexpected exceptions @@ -150,12 +170,32 @@ class LoginPageViewState extends ConsumerState _loggingIn = false; }); Logger().e(err, stackTrace: st); - unawaited(Sentry.captureException(err, stackTrace: st)); + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'login') + ..setTag('action', 'credentials_login'); + }, + ), + ); unawaited(ToastMessage.error(context, S.of(context).failed_login)); } else { // Not mounted: log and capture, avoid UI calls Logger().e(err, stackTrace: st); - unawaited(Sentry.captureException(err, stackTrace: st)); + unawaited( + Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'login') + ..setTag('action', 'credentials_login'); + }, + ), + ); } } } @@ -223,7 +263,15 @@ class LoginPageViewState extends ConsumerState }); } } catch (err, st) { - await Sentry.captureException(err, stackTrace: st); + await Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'login') + ..setTag('action', 'federated_login'); + }, + ); await closeInAppWebView(); if (mounted) { setState(() { diff --git a/packages/uni_app/lib/view/widgets/pages_layouts/general/general.dart b/packages/uni_app/lib/view/widgets/pages_layouts/general/general.dart index f2cf9db715..99ab3e9843 100644 --- a/packages/uni_app/lib/view/widgets/pages_layouts/general/general.dart +++ b/packages/uni_app/lib/view/widgets/pages_layouts/general/general.dart @@ -73,7 +73,16 @@ abstract class GeneralPageViewState }); } else { Logger().e('Failed to load page info: $err\n$st'); - await Sentry.captureException(err, stackTrace: st); + await Sentry.captureException( + err, + stackTrace: st, + withScope: (s) { + s + ..setTag('feature', 'general_page_layout') + ..setTag('action', 'onLoad') + ..setTag('page_title', getTitle() ?? 'unknown'); + }, + ); } } diff --git a/packages/uni_app/linux/flutter/generated_plugin_registrant.cc b/packages/uni_app/linux/flutter/generated_plugin_registrant.cc new file mode 100644 index 0000000000..397f5086a9 --- /dev/null +++ b/packages/uni_app/linux/flutter/generated_plugin_registrant.cc @@ -0,0 +1,43 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#include "generated_plugin_registrant.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_inappwebview_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterInappwebviewLinuxPlugin"); + flutter_inappwebview_linux_plugin_register_with_registrar(flutter_inappwebview_linux_registrar); + g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); + flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) gtk_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "GtkPlugin"); + gtk_plugin_register_with_registrar(gtk_registrar); + g_autoptr(FlPluginRegistrar) objectbox_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "ObjectboxFlutterLibsPlugin"); + objectbox_flutter_libs_plugin_register_with_registrar(objectbox_flutter_libs_registrar); + g_autoptr(FlPluginRegistrar) open_file_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "OpenFileLinuxPlugin"); + open_file_linux_plugin_register_with_registrar(open_file_linux_registrar); + g_autoptr(FlPluginRegistrar) sentry_flutter_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SentryFlutterPlugin"); + sentry_flutter_plugin_register_with_registrar(sentry_flutter_registrar); + g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); + url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); +} diff --git a/packages/uni_app/linux/flutter/generated_plugin_registrant.h b/packages/uni_app/linux/flutter/generated_plugin_registrant.h new file mode 100644 index 0000000000..e0f0a47bc0 --- /dev/null +++ b/packages/uni_app/linux/flutter/generated_plugin_registrant.h @@ -0,0 +1,15 @@ +// +// Generated file. Do not edit. +// + +// clang-format off + +#ifndef GENERATED_PLUGIN_REGISTRANT_ +#define GENERATED_PLUGIN_REGISTRANT_ + +#include + +// Registers Flutter plugins. +void fl_register_plugins(FlPluginRegistry* registry); + +#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/packages/uni_app/linux/flutter/generated_plugins.cmake b/packages/uni_app/linux/flutter/generated_plugins.cmake new file mode 100644 index 0000000000..469e308cf8 --- /dev/null +++ b/packages/uni_app/linux/flutter/generated_plugins.cmake @@ -0,0 +1,32 @@ +# +# Generated file, do not edit. +# + +list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux + flutter_inappwebview_linux + flutter_secure_storage_linux + gtk + objectbox_flutter_libs + open_file_linux + sentry_flutter + url_launcher_linux +) + +list(APPEND FLUTTER_FFI_PLUGIN_LIST + jni +) + +set(PLUGIN_BUNDLED_LIBRARIES) + +foreach(plugin ${FLUTTER_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) + target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) + list(APPEND PLUGIN_BUNDLED_LIBRARIES $) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) +endforeach(plugin) + +foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) + add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) + list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) +endforeach(ffi_plugin)