From dc0ef25d73834ce808fcc449344dba43e4228c0d Mon Sep 17 00:00:00 2001 From: otsmr Date: Fri, 29 May 2026 09:25:24 +0200 Subject: [PATCH] add optional database tracing --- .../database/drift_logging_interceptor.dart | 164 ++++++++++++++++++ lib/src/database/twonly.db.dart | 10 +- lib/src/model/json/userdata.model.dart | 3 + lib/src/model/json/userdata.model.g.dart | 2 + .../settings/developer/developer.view.dart | 14 ++ pubspec.yaml | 2 +- 6 files changed, 193 insertions(+), 2 deletions(-) create mode 100644 lib/src/database/drift_logging_interceptor.dart diff --git a/lib/src/database/drift_logging_interceptor.dart b/lib/src/database/drift_logging_interceptor.dart new file mode 100644 index 00000000..63ff94eb --- /dev/null +++ b/lib/src/database/drift_logging_interceptor.dart @@ -0,0 +1,164 @@ +import 'dart:async'; +import 'package:drift/drift.dart'; +import 'package:twonly/locator.dart'; +import 'package:twonly/src/utils/log.dart'; + +class DriftLoggingInterceptor extends QueryInterceptor { + bool get _isEnabled { + try { + if (!userService.isUserCreated) return false; + return userService.currentUser.enableDatabaseLogging; + } catch (_) { + return false; + } + } + + List _findUuids(dynamic value) { + if (value == null) return const []; + final uuids = []; + final uuidRegex = RegExp( + '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}', + ); + if (value is String) { + for (final match in uuidRegex.allMatches(value)) { + uuids.add(match.group(0)!); + } + } else if (value is Iterable) { + for (final element in value) { + uuids.addAll(_findUuids(element)); + } + } else if (value is Map) { + for (final element in value.values) { + uuids.addAll(_findUuids(element)); + } + } else { + final str = value.toString(); + for (final match in uuidRegex.allMatches(str)) { + uuids.add(match.group(0)!); + } + } + return uuids.toSet().toList(); + } + + Future _run( + String operation, + String statement, + List args, + Future Function() query, + ) async { + if (!_isEnabled) { + return query(); + } + final stopwatch = Stopwatch()..start(); + try { + final result = await query(); + final elapsed = stopwatch.elapsedMilliseconds; + final uuids = _findUuids(args); + if (uuids.isNotEmpty) { + Log.info( + '[DriftDB] $operation succeeded in ${elapsed}ms: "$statement" | UUIDs: $uuids', + ); + } else { + Log.info( + '[DriftDB] $operation succeeded in ${elapsed}ms: "$statement"', + ); + } + return result; + } catch (e) { + final elapsed = stopwatch.elapsedMilliseconds; + final uuids = _findUuids(args); + if (uuids.isNotEmpty) { + Log.info( + '[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement" | UUIDs: $uuids', + ); + } else { + Log.info( + '[DriftDB] $operation failed after ${elapsed}ms ($e): "$statement"', + ); + } + rethrow; + } + } + + @override + Future runInsert( + QueryExecutor executor, + String statement, + List args, + ) { + return _run('INSERT', statement, args, () => executor.runInsert(statement, args)); + } + + @override + Future runUpdate( + QueryExecutor executor, + String statement, + List args, + ) { + return _run('UPDATE', statement, args, () => executor.runUpdate(statement, args)); + } + + @override + Future runDelete( + QueryExecutor executor, + String statement, + List args, + ) { + return _run('DELETE', statement, args, () => executor.runDelete(statement, args)); + } + + @override + Future runCustom( + QueryExecutor executor, + String statement, + List args, + ) { + return _run('CUSTOM', statement, args, () => executor.runCustom(statement, args)); + } + + @override + Future runBatched( + QueryExecutor executor, + BatchedStatements statements, + ) async { + if (!_isEnabled) { + return executor.runBatched(statements); + } + final stopwatch = Stopwatch()..start(); + try { + await executor.runBatched(statements); + final elapsed = stopwatch.elapsedMilliseconds; + final uuids = []; + for (final batchArg in statements.arguments) { + uuids.addAll(_findUuids(batchArg.arguments)); + } + final statementsStr = statements.statements.join('; '); + if (uuids.isNotEmpty) { + Log.info( + '[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr" | UUIDs: $uuids', + ); + } else { + Log.info( + '[DriftDB] BATCH succeeded in ${elapsed}ms: "$statementsStr"', + ); + } + } catch (e) { + final elapsed = stopwatch.elapsedMilliseconds; + final uuids = []; + for (final batchArg in statements.arguments) { + uuids.addAll(_findUuids(batchArg.arguments)); + } + final statementsStr = statements.statements.join('; '); + if (uuids.isNotEmpty) { + Log.info( + '[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr" | UUIDs: $uuids', + ); + } else { + Log.info( + '[DriftDB] BATCH failed after ${elapsed}ms ($e): "$statementsStr"', + ); + } + rethrow; + } + } +} diff --git a/lib/src/database/twonly.db.dart b/lib/src/database/twonly.db.dart index e1566f0c..b86d40b2 100644 --- a/lib/src/database/twonly.db.dart +++ b/lib/src/database/twonly.db.dart @@ -2,6 +2,7 @@ import 'package:drift/drift.dart'; import 'package:drift_flutter/drift_flutter.dart' show DriftNativeOptions, driftDatabase; import 'package:path_provider/path_provider.dart'; +import 'package:twonly/locator.dart'; import 'package:twonly/src/database/daos/contacts.dao.dart'; import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/daos/key_verification.dao.dart'; @@ -11,6 +12,7 @@ import 'package:twonly/src/database/daos/reactions.dao.dart'; import 'package:twonly/src/database/daos/receipts.dao.dart'; import 'package:twonly/src/database/daos/shortcuts.dao.dart'; import 'package:twonly/src/database/daos/user_discovery.dao.dart'; +import 'package:twonly/src/database/drift_logging_interceptor.dart'; import 'package:twonly/src/database/tables/contacts.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart'; @@ -83,7 +85,7 @@ class TwonlyDB extends _$TwonlyDB { int get schemaVersion => 17; static QueryExecutor _openConnection() { - return driftDatabase( + final connection = driftDatabase( name: 'twonly', native: DriftNativeOptions( databaseDirectory: getApplicationSupportDirectory, @@ -96,6 +98,12 @@ class TwonlyDB extends _$TwonlyDB { }, ), ); + try { + if (userService.isUserCreated && userService.currentUser.enableDatabaseLogging) { + return connection.interceptWith(DriftLoggingInterceptor()); + } + } catch (_) {} + return connection; } @override diff --git a/lib/src/model/json/userdata.model.dart b/lib/src/model/json/userdata.model.dart index e3aa59a6..291603c2 100644 --- a/lib/src/model/json/userdata.model.dart +++ b/lib/src/model/json/userdata.model.dart @@ -64,6 +64,9 @@ class UserData { @JsonKey(defaultValue: false) bool requestedAudioPermission = false; + @JsonKey(defaultValue: false) + bool enableDatabaseLogging = false; + @JsonKey(defaultValue: false) bool automaticallyMarkEqualMediaFilesAsOpened = false; diff --git a/lib/src/model/json/userdata.model.g.dart b/lib/src/model/json/userdata.model.g.dart index 4ae054fd..9cec2fea 100644 --- a/lib/src/model/json/userdata.model.g.dart +++ b/lib/src/model/json/userdata.model.g.dart @@ -42,6 +42,7 @@ UserData _$UserDataFromJson(Map json) => ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..requestedAudioPermission = json['requestedAudioPermission'] as bool? ?? false + ..enableDatabaseLogging = json['enableDatabaseLogging'] as bool? ?? false ..automaticallyMarkEqualMediaFilesAsOpened = json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false ..videoStabilizationEnabled = @@ -135,6 +136,7 @@ Map _$UserDataToJson(UserData instance) => { 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'defaultShowTime': instance.defaultShowTime, 'requestedAudioPermission': instance.requestedAudioPermission, + 'enableDatabaseLogging': instance.enableDatabaseLogging, 'automaticallyMarkEqualMediaFilesAsOpened': instance.automaticallyMarkEqualMediaFilesAsOpened, 'videoStabilizationEnabled': instance.videoStabilizationEnabled, diff --git a/lib/src/visual/views/settings/developer/developer.view.dart b/lib/src/visual/views/settings/developer/developer.view.dart index 8200b965..1063e6d7 100644 --- a/lib/src/visual/views/settings/developer/developer.view.dart +++ b/lib/src/visual/views/settings/developer/developer.view.dart @@ -263,6 +263,12 @@ class _DeveloperSettingsViewState extends State { ); } + Future toggleDatabaseLogging() async { + await UserService.update( + (u) => u.enableDatabaseLogging = !u.enableDatabaseLogging, + ); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -282,6 +288,14 @@ class _DeveloperSettingsViewState extends State { onChanged: (_) => toggleDeveloperSettings(), ), ), + ListTile( + title: const Text('Enable Database Logging'), + onTap: toggleDatabaseLogging, + trailing: Switch( + value: userService.currentUser.enableDatabaseLogging, + onChanged: (_) => toggleDatabaseLogging(), + ), + ), ListTile( title: const Text('User ID'), subtitle: Text(userService.currentUser.userId.toString()), diff --git a/pubspec.yaml b/pubspec.yaml index 1294c242..640788bf 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec publish_to: 'none' -version: 0.2.23+132 +version: 0.2.24+133 environment: sdk: ^3.11.0