adds optional sentry #299

This commit is contained in:
otsmr 2025-11-10 00:04:44 +01:00
parent 6d86af155c
commit 091e4bbfa8
18 changed files with 168 additions and 37 deletions

View file

@ -33,3 +33,4 @@ void Function(SubscriptionPlan plan) globalCallbackUpdatePlan =
Map<String, VoidCallback> globalUserDataChangedCallBack = {}; Map<String, VoidCallback> globalUserDataChangedCallBack = {};
bool globalIsAppInBackground = true; bool globalIsAppInBackground = true;
bool globalAllowErrorTrackingViaSentry = false;

View file

@ -9,6 +9,7 @@ import 'package:flutter/services.dart';
import 'package:path/path.dart'; import 'package:path/path.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/app.dart'; import 'package:twonly/app.dart';
import 'package:twonly/globals.dart'; import 'package:twonly/globals.dart';
import 'package:twonly/src/database/twonly.db.dart'; import 'package:twonly/src/database/twonly.db.dart';
@ -69,13 +70,34 @@ void main() async {
unawaited(createPushAvatars()); unawaited(createPushAvatars());
await twonlyDB.messagesDao.purgeMessageTable(); 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( runApp(
MultiProvider( MultiProvider(
providers: [ providers: providers,
ChangeNotifierProvider(create: (_) => settingsController),
ChangeNotifierProvider(create: (_) => CustomChangeProvider()),
ChangeNotifierProvider(create: (_) => ImageEditorProvider()),
],
child: const App(), child: const App(),
), ),
); );

View file

@ -57,7 +57,6 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
tbl.preKeyId.equals(preKey.preKeyId), tbl.preKeyId.equals(preKey.preKeyId),
)) ))
.go(); .go();
Log.info('[PREKEY] Using prekey ${preKey.preKeyId} for $contactId');
return preKey; return preKey;
} }
return null; return null;
@ -68,7 +67,6 @@ class SignalDao extends DatabaseAccessor<TwonlyDB> with _$SignalDaoMixin {
List<SignalContactPreKeysCompanion> preKeys, List<SignalContactPreKeysCompanion> preKeys,
) async { ) async {
for (final preKey in preKeys) { for (final preKey in preKeys) {
Log.info('[PREKEY] Inserting others ${preKey.preKeyId}');
try { try {
await into(signalContactPreKeys).insert(preKey); await into(signalContactPreKeys).insert(preKey);
} catch (e) { } catch (e) {

View file

@ -23,14 +23,12 @@ class ConnectPreKeyStore extends PreKeyStore {
'[PREKEY] No such preKey record! - $preKeyId', '[PREKEY] No such preKey record! - $preKeyId',
); );
} }
Log.info('[PREKEY] Contact used my preKey $preKeyId');
final preKey = preKeyRecord.first.preKey; final preKey = preKeyRecord.first.preKey;
return PreKeyRecord.fromBuffer(preKey); return PreKeyRecord.fromBuffer(preKey);
} }
@override @override
Future<void> removePreKey(int preKeyId) async { Future<void> removePreKey(int preKeyId) async {
Log.info('[PREKEY] Removing $preKeyId from my own storage.');
await (twonlyDB.delete(twonlyDB.signalPreKeyStores) await (twonlyDB.delete(twonlyDB.signalPreKeyStores)
..where((tbl) => tbl.preKeyId.equals(preKeyId))) ..where((tbl) => tbl.preKeyId.equals(preKeyId)))
.go(); .go();
@ -43,7 +41,6 @@ class ConnectPreKeyStore extends PreKeyStore {
preKey: Value(record.serialize()), preKey: Value(record.serialize()),
); );
Log.info('[PREKEY] Storing $preKeyId from my own storage.');
try { try {
await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion); await twonlyDB.into(twonlyDB.signalPreKeyStores).insert(preKeyCompanion);
} catch (e) { } catch (e) {

View file

@ -820,5 +820,7 @@
"yourTwonlyScore": "Dein twonly-Score", "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.", "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?", "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."
} }

View file

@ -598,5 +598,7 @@
"yourTwonlyScore": "Your twonly-Score", "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.", "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?", "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."
} }

View file

@ -2695,6 +2695,18 @@ abstract class AppLocalizations {
/// In en, this message translates to: /// In en, this message translates to:
/// **'Delete'** /// **'Delete'**
String get dialogAskDeleteMediaFilePopDelete; 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 class _AppLocalizationsDelegate

View file

@ -1488,4 +1488,11 @@ class AppLocalizationsDe extends AppLocalizations {
@override @override
String get dialogAskDeleteMediaFilePopDelete => 'Löschen'; 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.';
} }

View file

@ -1478,4 +1478,11 @@ class AppLocalizationsEn extends AppLocalizations {
@override @override
String get dialogAskDeleteMediaFilePopDelete => 'Delete'; 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.';
} }

View file

@ -48,9 +48,6 @@ class UserData {
int? defaultShowTime; int? defaultShowTime;
@JsonKey(defaultValue: true)
bool useHighQuality = true;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool requestedAudioPermission = false; bool requestedAudioPermission = false;
@ -73,6 +70,9 @@ class UserData {
DateTime? signalLastSignedPreKeyUpdated; DateTime? signalLastSignedPreKeyUpdated;
@JsonKey(defaultValue: false)
bool allowErrorTrackingViaSentry = false;
// -- Custom DATA -- // -- Custom DATA --
@JsonKey(defaultValue: 100_000) @JsonKey(defaultValue: 100_000)

View file

@ -26,7 +26,6 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
$enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ?? $enumDecodeNullable(_$ThemeModeEnumMap, json['themeMode']) ??
ThemeMode.system ThemeMode.system
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..useHighQuality = json['useHighQuality'] as bool? ?? true
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true ..showFeedbackShortcut = json['showFeedbackShortcut'] as bool? ?? true
@ -50,6 +49,8 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) => UserData(
json['signalLastSignedPreKeyUpdated'] == null json['signalLastSignedPreKeyUpdated'] == null
? null ? null
: DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String) : DateTime.parse(json['signalLastSignedPreKeyUpdated'] as String)
..allowErrorTrackingViaSentry =
json['allowErrorTrackingViaSentry'] as bool? ?? false
..currentPreKeyIndexStart = ..currentPreKeyIndexStart =
(json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000 (json['currentPreKeyIndexStart'] as num?)?.toInt() ?? 100000
..currentSignedPreKeyIndexStart = ..currentSignedPreKeyIndexStart =
@ -84,7 +85,6 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'todaysImageCounter': instance.todaysImageCounter, 'todaysImageCounter': instance.todaysImageCounter,
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'useHighQuality': instance.useHighQuality,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'showFeedbackShortcut': instance.showFeedbackShortcut, 'showFeedbackShortcut': instance.showFeedbackShortcut,
'preSelectedEmojies': instance.preSelectedEmojies, 'preSelectedEmojies': instance.preSelectedEmojies,
@ -96,6 +96,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'myBestFriendGroupId': instance.myBestFriendGroupId, 'myBestFriendGroupId': instance.myBestFriendGroupId,
'signalLastSignedPreKeyUpdated': 'signalLastSignedPreKeyUpdated':
instance.signalLastSignedPreKeyUpdated?.toIso8601String(), instance.signalLastSignedPreKeyUpdated?.toIso8601String(),
'allowErrorTrackingViaSentry': instance.allowErrorTrackingViaSentry,
'currentPreKeyIndexStart': instance.currentPreKeyIndexStart, 'currentPreKeyIndexStart': instance.currentPreKeyIndexStart,
'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart, 'currentSignedPreKeyIndexStart': instance.currentSignedPreKeyIndexStart,
'lastChangeLogHash': instance.lastChangeLogHash, 'lastChangeLogHash': instance.lastChangeLogHash,

View file

@ -248,7 +248,7 @@ Future<void> requestMediaReupload(String mediaId) async {
Future<void> handleEncryptedFile(String mediaId) async { Future<void> handleEncryptedFile(String mediaId) async {
final mediaService = await MediaFileService.fromMediaId(mediaId); final mediaService = await MediaFileService.fromMediaId(mediaId);
if (mediaService == null) { if (mediaService == null) {
Log.error('Media file $mediaId not found in database.'); Log.error('Media file not found in database.');
return; return;
} }
@ -263,7 +263,7 @@ Future<void> handleEncryptedFile(String mediaId) async {
try { try {
encryptedBytes = await mediaService.encryptedPath.readAsBytes(); encryptedBytes = await mediaService.encryptedPath.readAsBytes();
} catch (e) { } catch (e) {
Log.error('Could not read encrypted media file: $mediaId. $e'); Log.error('Could not read encrypted media file: $e');
await requestMediaReupload(mediaId); await requestMediaReupload(mediaId);
return; return;
} }

View file

@ -44,14 +44,14 @@ Future<(Uint8List, Uint8List?)?> tryToSendCompleteMessage({
if (receipt == null) { if (receipt == null) {
receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!); receipt = await twonlyDB.receiptsDao.getReceiptById(receiptId!);
if (receipt == null) { if (receipt == null) {
Log.error('Receipt $receiptId not found.'); Log.error('Receipt not found.');
return null; return null;
} }
} }
receiptId = receipt.receiptId; receiptId = receipt.receiptId;
if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) { if (!onlyReturnEncryptedData && receipt.ackByServerAt != null) {
Log.error('$receiptId message already uploaded!'); Log.error('message already uploaded!');
return null; return null;
} }

View file

@ -3,6 +3,8 @@ import 'package:flutter/foundation.dart';
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:mutex/mutex.dart'; import 'package:mutex/mutex.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';
import 'package:twonly/globals.dart';
void initLogger() { void initLogger() {
// Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL; // Logger.root.level = kReleaseMode ? Level.INFO : Level.ALL;
@ -20,6 +22,13 @@ void initLogger() {
class Log { class Log {
static void error(Object? message, [Object? error, StackTrace? stackTrace]) { 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); Logger(_getCallerSourceCodeFilename()).shout(message, error, stackTrace);
} }
@ -45,6 +54,15 @@ Future<String> loadLogFile() async {
} }
} }
Future<String> 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<void> _writeLogToFile(LogRecord record) async { Future<void> _writeLogToFile(LogRecord record) async {
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
final logFile = File('${directory.path}/app.log'); final logFile = File('${directory.path}/app.log');

View file

@ -28,7 +28,7 @@ class _DiagnosticsViewState extends State<DiagnosticsView> {
return Scaffold( return Scaffold(
appBar: AppBar(title: const Text('Diagnostics')), appBar: AppBar(title: const Text('Diagnostics')),
body: FutureBuilder<String>( body: FutureBuilder<String>(
future: loadLogFile(), future: readLast1000Lines(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());

View file

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart';
import 'package:package_info_plus/package_info_plus.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/misc.dart';
import 'package:twonly/src/utils/storage.dart'; import 'package:twonly/src/utils/storage.dart';
import 'package:twonly/src/views/components/alert_dialog.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:twonly/src/views/settings/help/faq.view.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
class HelpView extends StatelessWidget { class HelpView extends StatefulWidget {
const HelpView({super.key}); const HelpView({super.key});
@override
State<HelpView> createState() => _HelpViewState();
}
class _HelpViewState extends State<HelpView> {
Future<void> toggleAllowErrorTrackingViaSentry() async {
await updateUserdata((u) {
u.allowErrorTrackingViaSentry = !u.allowErrorTrackingViaSentry;
return u;
});
setState(() {});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -49,7 +64,10 @@ class HelpView extends StatelessWidget {
), ),
ListTile( ListTile(
title: Text(context.lang.settingsResetTutorials), title: Text(context.lang.settingsResetTutorials),
subtitle: Text(context.lang.settingsResetTutorialsDesc), subtitle: Text(
context.lang.settingsResetTutorialsDesc,
style: const TextStyle(fontSize: 12),
),
onTap: () async { onTap: () async {
await updateUserdata((user) { await updateUserdata((user) {
user.tutorialDisplayed = []; user.tutorialDisplayed = [];
@ -65,6 +83,32 @@ class HelpView extends StatelessWidget {
}, },
), ),
const Divider(), 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( FutureBuilder(
future: PackageInfo.fromPlatform(), future: PackageInfo.fromPlatform(),
builder: (context, snap) { 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( ListTile(
title: const Text('Changelog'), title: const Text('Changelog'),
onTap: () async { onTap: () async {

View file

@ -976,6 +976,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.0.5" version: "1.0.5"
jni:
dependency: transitive
description:
name: jni
sha256: d2c361082d554d4593c3012e26f6b188f902acd291330f13d6427641a92b3da1
url: "https://pub.dev"
source: hosted
version: "0.14.2"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -1160,6 +1168,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.1" 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: octo_image:
dependency: transitive dependency: transitive
description: description:
@ -1440,6 +1456,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.3.8" 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: share_plus:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -69,6 +69,7 @@ dependencies:
restart_app: ^1.3.2 restart_app: ^1.3.2
screenshot: ^3.0.0 screenshot: ^3.0.0
scrollable_positioned_list: ^0.3.8 scrollable_positioned_list: ^0.3.8
sentry_flutter: ^9.8.0
share_plus: ^12.0.0 share_plus: ^12.0.0
tutorial_coach_mark: ^1.3.0 tutorial_coach_mark: ^1.3.0
url_launcher: ^6.3.1 url_launcher: ^6.3.1