diff --git a/lib/globals.dart b/lib/globals.dart index 5a9acbf..279e447 100644 --- a/lib/globals.dart +++ b/lib/globals.dart @@ -33,3 +33,4 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan = Map globalUserDataChangedCallBack = {}; bool globalIsAppInBackground = true; +bool globalAllowErrorTrackingViaSentry = false; diff --git a/lib/main.dart b/lib/main.dart index 7ff34fd..76b2f4d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -9,6 +9,7 @@ import 'package:flutter/services.dart'; import 'package:path/path.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; import 'package:twonly/app.dart'; import 'package:twonly/globals.dart'; import 'package:twonly/src/database/twonly.db.dart'; @@ -69,13 +70,34 @@ void main() async { unawaited(createPushAvatars()); await twonlyDB.messagesDao.purgeMessageTable(); + final providers = [ + ChangeNotifierProvider(create: (_) => settingsController), + ChangeNotifierProvider(create: (_) => CustomChangeProvider()), + ChangeNotifierProvider(create: (_) => ImageEditorProvider()), + ]; + + if (user != null) { + if (user.allowErrorTrackingViaSentry) { + globalAllowErrorTrackingViaSentry = true; + return SentryFlutter.init( + (options) => options + ..dsn = + 'https://6b24a012c85144c9b522440a1d17d01c@glitchtip.twonly.eu/4' + ..tracesSampleRate = 0.01 + ..enableAutoSessionTracking = false, + appRunner: () => runApp( + MultiProvider( + providers: providers, + child: const App(), + ), + ), + ); + } + } + runApp( MultiProvider( - providers: [ - ChangeNotifierProvider(create: (_) => settingsController), - ChangeNotifierProvider(create: (_) => CustomChangeProvider()), - ChangeNotifierProvider(create: (_) => ImageEditorProvider()), - ], + providers: providers, child: const App(), ), ); diff --git a/lib/src/database/daos/signal.dao.dart b/lib/src/database/daos/signal.dao.dart index 2b64aac..294e358 100644 --- a/lib/src/database/daos/signal.dao.dart +++ b/lib/src/database/daos/signal.dao.dart @@ -57,7 +57,6 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { tbl.preKeyId.equals(preKey.preKeyId), )) .go(); - Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId'); return preKey; } return null; @@ -68,7 +67,6 @@ class SignalDao extends DatabaseAccessor with _$SignalDaoMixin { List preKeys, ) async { for (final preKey in preKeys) { - Log.info('[PREKEY] Inserting others ${preKey.preKeyId}'); try { await into(signalContactPreKeys).insert(preKey); } catch (e) { diff --git a/lib/src/database/signal/connect_pre_key_store.dart b/lib/src/database/signal/connect_pre_key_store.dart index 60018c5..61846c3 100644 --- a/lib/src/database/signal/connect_pre_key_store.dart +++ b/lib/src/database/signal/connect_pre_key_store.dart @@ -23,14 +23,12 @@ class ConnectPreKeyStore extends PreKeyStore { '[PREKEY] No such preKey record! - $preKeyId', ); } - Log.info('[PREKEY] Contact used my preKey $preKeyId'); final preKey = preKeyRecord.first.preKey; return PreKeyRecord.fromBuffer(preKey); } @override Future removePreKey(int preKeyId) async { - Log.info('[PREKEY] Removing $preKeyId from my own storage.'); await (twonlyDB.delete(twonlyDB.signalPreKeyStores) ..where((tbl) => tbl.preKeyId.equals(preKeyId))) .go(); @@ -43,7 +41,6 @@ class ConnectPreKeyStore extends PreKeyStore { preKey: Value(record.serialize()), ); - Log.info('[PREKEY] Storing $preKeyId from my own storage.'); try { await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); } catch (e) { diff --git a/lib/src/localization/app_de.arb b/lib/src/localization/app_de.arb index deaf7b0..5b1a748 100644 --- a/lib/src/localization/app_de.arb +++ b/lib/src/localization/app_de.arb @@ -820,5 +820,7 @@ "yourTwonlyScore": "Dein twonly-Score", "registrationClosed": "Aufgrund des aktuell sehr hohen Aufkommens haben wir die Registrierung vorübergehend deaktiviert, damit der Dienst zuverlässig bleibt. Bitte versuche es in ein paar Tagen noch einmal.", "dialogAskDeleteMediaFilePopTitle": "Bist du sicher, dass du dein Meisterwerk löschen möchtest?", - "dialogAskDeleteMediaFilePopDelete": "Löschen" + "dialogAskDeleteMediaFilePopDelete": "Löschen", + "allowErrorTracking": "Fehler und Crashes mit uns teilen", + "allowErrorTrackingSubtitle": "Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen." } \ No newline at end of file diff --git a/lib/src/localization/app_en.arb b/lib/src/localization/app_en.arb index c122b12..bc772d9 100644 --- a/lib/src/localization/app_en.arb +++ b/lib/src/localization/app_en.arb @@ -598,5 +598,7 @@ "yourTwonlyScore": "Your twonly-Score", "registrationClosed": "Due to the current high volume of registrations, we have temporarily disabled registration to ensure that the service remains reliable. Please try again in a few days.", "dialogAskDeleteMediaFilePopTitle": "Are you sure you want to delete your masterpiece?", - "dialogAskDeleteMediaFilePopDelete": "Delete" + "dialogAskDeleteMediaFilePopDelete": "Delete", + "allowErrorTracking": "Share errors and crashes with us", + "allowErrorTrackingSubtitle": "If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded." } \ No newline at end of file diff --git a/lib/src/localization/generated/app_localizations.dart b/lib/src/localization/generated/app_localizations.dart index 549cd0d..1e8274f 100644 --- a/lib/src/localization/generated/app_localizations.dart +++ b/lib/src/localization/generated/app_localizations.dart @@ -2695,6 +2695,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Delete'** String get dialogAskDeleteMediaFilePopDelete; + + /// No description provided for @allowErrorTracking. + /// + /// In en, this message translates to: + /// **'Share errors and crashes with us'** + String get allowErrorTracking; + + /// No description provided for @allowErrorTrackingSubtitle. + /// + /// In en, this message translates to: + /// **'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'** + String get allowErrorTrackingSubtitle; } class _AppLocalizationsDelegate diff --git a/lib/src/localization/generated/app_localizations_de.dart b/lib/src/localization/generated/app_localizations_de.dart index ce54fd6..b5fc863 100644 --- a/lib/src/localization/generated/app_localizations_de.dart +++ b/lib/src/localization/generated/app_localizations_de.dart @@ -1488,4 +1488,11 @@ class AppLocalizationsDe extends AppLocalizations { @override String get dialogAskDeleteMediaFilePopDelete => 'Löschen'; + + @override + String get allowErrorTracking => 'Fehler und Crashes mit uns teilen'; + + @override + String get allowErrorTrackingSubtitle => + 'Wenn twonly abstürzt oder Fehler auftreten, werden diese automatisch an unsere selbst gehostete Glitchtip-Instanz gemeldet. Persönliche Daten wie Nachrichten oder Bilder werden niemals hochgeladen.'; } diff --git a/lib/src/localization/generated/app_localizations_en.dart b/lib/src/localization/generated/app_localizations_en.dart index 180e7fc..0fb3086 100644 --- a/lib/src/localization/generated/app_localizations_en.dart +++ b/lib/src/localization/generated/app_localizations_en.dart @@ -1478,4 +1478,11 @@ class AppLocalizationsEn extends AppLocalizations { @override String get dialogAskDeleteMediaFilePopDelete => 'Delete'; + + @override + String get allowErrorTracking => 'Share errors and crashes with us'; + + @override + String get allowErrorTrackingSubtitle => + 'If twonly crashes or errors occur, these are automatically reported to our self-hosted Glitchtip instance. Personal data such as messages or images are never uploaded.'; } diff --git a/lib/src/model/json/userdata.dart b/lib/src/model/json/userdata.dart index 604c273..ca667ac 100644 --- a/lib/src/model/json/userdata.dart +++ b/lib/src/model/json/userdata.dart @@ -48,9 +48,6 @@ class UserData { int? defaultShowTime; - @JsonKey(defaultValue: true) - bool useHighQuality = true; - @JsonKey(defaultValue: false) bool requestedAudioPermission = false; @@ -73,6 +70,9 @@ class UserData { DateTime? signalLastSignedPreKeyUpdated; + @JsonKey(defaultValue: false) + bool allowErrorTrackingViaSentry = false; + // -- Custom DATA -- @JsonKey(defaultValue: 100_000) diff --git a/lib/src/model/json/userdata.g.dart b/lib/src/model/json/userdata.g.dart index 6de5cbd..fd997f0 100644 --- a/lib/src/model/json/userdata.g.dart +++ b/lib/src/model/json/userdata.g.dart @@ -26,7 +26,6 @@ UserData _$UserDataFromJson(Map json) => UserData( $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? ThemeMode.system ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() - ..useHighQuality = json['useHighQuality'] as bool? ?? true ..requestedAudioPermission = json['requestedAudioPermission'] as bool? ?? false ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true @@ -50,6 +49,8 @@ UserData _$UserDataFromJson(Map json) => UserData( json['signalLastSignedPreKeyUpdated'] == null ? null : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) + ..allowErrorTrackingViaSentry = + json['allowErrorTrackingViaSentry'] as bool? ?? false ..currentPreKeyIndexStart = (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 ..currentSignedPreKeyIndexStart = @@ -84,7 +85,6 @@ Map _$UserDataToJson(UserData instance) => { 'todaysImageCounter': instance.todaysImageCounter, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'defaultShowTime': instance.defaultShowTime, - 'useHighQuality': instance.useHighQuality, 'requestedAudioPermission': instance.requestedAudioPermission, 'showFeedbackShortcut': instance.showFeedbackShortcut, 'preSelectedEmojies': instance.preSelectedEmojies, @@ -96,6 +96,7 @@ Map _$UserDataToJson(UserData instance) => { 'myBestFriendGroupId': instance.myBestFriendGroupId, 'signalLastSignedPreKeyUpdated': instance.signalLastSignedPreKeyUpdated?.toIso8601String(), + 'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'lastChangeLogHash': instance.lastChangeLogHash, diff --git a/lib/src/services/api/mediafiles/download.service.dart b/lib/src/services/api/mediafiles/download.service.dart index ec8f659..b5ff2ad 100644 --- a/lib/src/services/api/mediafiles/download.service.dart +++ b/lib/src/services/api/mediafiles/download.service.dart @@ -248,7 +248,7 @@ Future requestMediaReupload(String mediaId) async { Future handleEncryptedFile(String mediaId) async { final mediaService = await MediaFileService.fromMediaId(mediaId); if (mediaService == null) { - Log.error('Media file $mediaId not found in database.'); + Log.error('Media file not found in database.'); return; } @@ -263,7 +263,7 @@ Future handleEncryptedFile(String mediaId) async { try { encryptedBytes = await mediaService.encryptedPath.readAsBytes(); } catch (e) { - Log.error('Could not read encrypted media file: $mediaId. $e'); + Log.error('Could not read encrypted media file: $e'); await requestMediaReupload(mediaId); return; } diff --git a/lib/src/services/api/messages.dart b/lib/src/services/api/messages.dart index b2503fc..4a8cb43 100644 --- a/lib/src/services/api/messages.dart +++ b/lib/src/services/api/messages.dart @@ -44,14 +44,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({ if (receipt == null) { receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); if (receipt == null) { - Log.error('Receipt $receiptId not found.'); + Log.error('Receipt not found.'); return null; } } receiptId = receipt.receiptId; if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { - Log.error('$receiptId message already uploaded!'); + Log.error('message already uploaded!'); return null; } diff --git a/lib/src/utils/log.dart b/lib/src/utils/log.dart index af28bc6..fa9c34c 100644 --- a/lib/src/utils/log.dart +++ b/lib/src/utils/log.dart @@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:path_provider/path_provider.dart'; +import 'package:sentry_flutter/sentry_flutter.dart'; +import 'package:twonly/globals.dart'; void initLogger() { // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; @@ -20,6 +22,13 @@ void initLogger() { class Log { static void error(Object? message, [Object? error, StackTrace? stackTrace]) { + if (globalAllowErrorTrackingViaSentry) { + try { + throw Exception(message); + } catch (exception, stackTrace) { + Sentry.captureException(exception, stackTrace: stackTrace); + } + } Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace); } @@ -45,6 +54,15 @@ Future loadLogFile() async { } } +Future readLast1000Lines() async { + final dir = await getApplicationSupportDirectory(); + final file = File('${dir.path}/app.log'); + if (!file.existsSync()) return ''; + final all = await file.readAsLines(); + final start = all.length > 1000 ? all.length - 1000 : 0; + return all.sublist(start).join('\n'); +} + Future _writeLogToFile(LogRecord record) async { final directory = await getApplicationSupportDirectory(); final logFile = File('${directory.path}/app.log'); diff --git a/lib/src/views/settings/help/diagnostics.view.dart b/lib/src/views/settings/help/diagnostics.view.dart index 7f27dd8..a25685a 100644 --- a/lib/src/views/settings/help/diagnostics.view.dart +++ b/lib/src/views/settings/help/diagnostics.view.dart @@ -28,7 +28,7 @@ class _DiagnosticsViewState extends State { return Scaffold( appBar: AppBar(title: const Text('Diagnostics')), body: FutureBuilder( - future: loadLogFile(), + future: readLast1000Lines(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); diff --git a/lib/src/views/settings/help/help.view.dart b/lib/src/views/settings/help/help.view.dart index a0bef20..cbffd12 100644 --- a/lib/src/views/settings/help/help.view.dart +++ b/lib/src/views/settings/help/help.view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:package_info_plus/package_info_plus.dart'; +import 'package:twonly/globals.dart'; import 'package:twonly/src/utils/misc.dart'; import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/views/components/alert_dialog.dart'; @@ -11,8 +12,22 @@ import 'package:twonly/src/views/settings/help/diagnostics.view.dart'; import 'package:twonly/src/views/settings/help/faq.view.dart'; import 'package:url_launcher/url_launcher.dart'; -class HelpView extends StatelessWidget { +class HelpView extends StatefulWidget { const HelpView({super.key}); + + @override + State createState() => _HelpViewState(); +} + +class _HelpViewState extends State { + Future toggleAllowErrorTrackingViaSentry() async { + await updateUserdata((u) { + u.allowErrorTrackingViaSentry = !u.allowErrorTrackingViaSentry; + return u; + }); + setState(() {}); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -49,7 +64,10 @@ class HelpView extends StatelessWidget { ), ListTile( title: Text(context.lang.settingsResetTutorials), - subtitle: Text(context.lang.settingsResetTutorialsDesc), + subtitle: Text( + context.lang.settingsResetTutorialsDesc, + style: const TextStyle(fontSize: 12), + ), onTap: () async { await updateUserdata((user) { user.tutorialDisplayed = []; @@ -65,6 +83,32 @@ class HelpView extends StatelessWidget { }, ), const Divider(), + ListTile( + title: Text(context.lang.allowErrorTracking), + subtitle: Text( + context.lang.allowErrorTrackingSubtitle, + style: const TextStyle(fontSize: 10), + ), + onTap: toggleAllowErrorTrackingViaSentry, + trailing: Switch( + value: gUser.allowErrorTrackingViaSentry, + onChanged: (a) => toggleAllowErrorTrackingViaSentry(), + ), + ), + ListTile( + title: Text(context.lang.settingsHelpDiagnostics), + onTap: () async { + await Navigator.push( + context, + MaterialPageRoute( + builder: (context) { + return const DiagnosticsView(); + }, + ), + ); + }, + ), + const Divider(), FutureBuilder( future: PackageInfo.fromPlatform(), builder: (context, snap) { @@ -97,19 +141,6 @@ class HelpView extends StatelessWidget { ); }, ), - ListTile( - title: Text(context.lang.settingsHelpDiagnostics), - onTap: () async { - await Navigator.push( - context, - MaterialPageRoute( - builder: (context) { - return const DiagnosticsView(); - }, - ), - ); - }, - ), ListTile( title: const Text('Changelog'), onTap: () async { diff --git a/pubspec.lock b/pubspec.lock index 63801d5..76d323c 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -976,6 +976,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.5" + jni: + dependency: transitive + description: + name: jni + sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1 + url: "https://pub.dev" + source: hosted + version: "0.14.2" js: dependency: transitive description: @@ -1160,6 +1168,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.1" + objective_c: + dependency: transitive + description: + name: objective_c + sha256: "64e35e1e2e79da4e83f2ace3bf4e5437cef523f46c7db2eba9a1419c49573790" + url: "https://pub.dev" + source: hosted + version: "8.0.0" octo_image: dependency: transitive description: @@ -1440,6 +1456,22 @@ packages: url: "https://pub.dev" source: hosted version: "0.3.8" + sentry: + dependency: transitive + description: + name: sentry + sha256: "10a0bc25f5f21468e3beeae44e561825aaa02cdc6829438e73b9b64658ff88d9" + url: "https://pub.dev" + source: hosted + version: "9.8.0" + sentry_flutter: + dependency: "direct main" + description: + name: sentry_flutter + sha256: aafbf41c63c98a30b17bdbf3313424d5102db62b08735c44bff810f277e786a5 + url: "https://pub.dev" + source: hosted + version: "9.8.0" share_plus: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 284db00..2222fd8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -69,6 +69,7 @@ dependencies: restart_app: ^1.3.2 screenshot: ^3.0.0 scrollable_positioned_list: ^0.3.8 + sentry_flutter: ^9.8.0 share_plus: ^12.0.0 tutorial_coach_mark: ^1.3.0 url_launcher: ^6.3.1