add optional database tracing
Some checks failed
Flutter analyze & test / flutter_analyze_and_test (push) Has been cancelled

This commit is contained in:
otsmr 2026-05-29 09:25:24 +02:00
parent 7559434f86
commit dc0ef25d73
6 changed files with 193 additions and 2 deletions

View file

@ -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<String> _findUuids(dynamic value) {
if (value == null) return const [];
final uuids = <String>[];
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<T> _run<T>(
String operation,
String statement,
List<Object?> args,
Future<T> 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<int> runInsert(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('INSERT', statement, args, () => executor.runInsert(statement, args));
}
@override
Future<int> runUpdate(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('UPDATE', statement, args, () => executor.runUpdate(statement, args));
}
@override
Future<int> runDelete(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('DELETE', statement, args, () => executor.runDelete(statement, args));
}
@override
Future<void> runCustom(
QueryExecutor executor,
String statement,
List<Object?> args,
) {
return _run('CUSTOM', statement, args, () => executor.runCustom(statement, args));
}
@override
Future<void> 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 = <String>[];
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 = <String>[];
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;
}
}
}

View file

@ -2,6 +2,7 @@ import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart' import 'package:drift_flutter/drift_flutter.dart'
show DriftNativeOptions, driftDatabase; show DriftNativeOptions, driftDatabase;
import 'package:path_provider/path_provider.dart'; 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/contacts.dao.dart';
import 'package:twonly/src/database/daos/groups.dao.dart'; import 'package:twonly/src/database/daos/groups.dao.dart';
import 'package:twonly/src/database/daos/key_verification.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/receipts.dao.dart';
import 'package:twonly/src/database/daos/shortcuts.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/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/contacts.table.dart';
import 'package:twonly/src/database/tables/groups.table.dart'; import 'package:twonly/src/database/tables/groups.table.dart';
import 'package:twonly/src/database/tables/mediafiles.table.dart'; import 'package:twonly/src/database/tables/mediafiles.table.dart';
@ -83,7 +85,7 @@ class TwonlyDB extends _$TwonlyDB {
int get schemaVersion => 17; int get schemaVersion => 17;
static QueryExecutor _openConnection() { static QueryExecutor _openConnection() {
return driftDatabase( final connection = driftDatabase(
name: 'twonly', name: 'twonly',
native: DriftNativeOptions( native: DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory, 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 @override

View file

@ -64,6 +64,9 @@ class UserData {
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool requestedAudioPermission = false; bool requestedAudioPermission = false;
@JsonKey(defaultValue: false)
bool enableDatabaseLogging = false;
@JsonKey(defaultValue: false) @JsonKey(defaultValue: false)
bool automaticallyMarkEqualMediaFilesAsOpened = false; bool automaticallyMarkEqualMediaFilesAsOpened = false;

View file

@ -42,6 +42,7 @@ UserData _$UserDataFromJson(Map<String, dynamic> json) =>
..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt() ..defaultShowTime = (json['defaultShowTime'] as num?)?.toInt()
..requestedAudioPermission = ..requestedAudioPermission =
json['requestedAudioPermission'] as bool? ?? false json['requestedAudioPermission'] as bool? ?? false
..enableDatabaseLogging = json['enableDatabaseLogging'] as bool? ?? false
..automaticallyMarkEqualMediaFilesAsOpened = ..automaticallyMarkEqualMediaFilesAsOpened =
json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false json['automaticallyMarkEqualMediaFilesAsOpened'] as bool? ?? false
..videoStabilizationEnabled = ..videoStabilizationEnabled =
@ -135,6 +136,7 @@ Map<String, dynamic> _$UserDataToJson(UserData instance) => <String, dynamic>{
'themeMode': _$ThemeModeEnumMap[instance.themeMode]!, 'themeMode': _$ThemeModeEnumMap[instance.themeMode]!,
'defaultShowTime': instance.defaultShowTime, 'defaultShowTime': instance.defaultShowTime,
'requestedAudioPermission': instance.requestedAudioPermission, 'requestedAudioPermission': instance.requestedAudioPermission,
'enableDatabaseLogging': instance.enableDatabaseLogging,
'automaticallyMarkEqualMediaFilesAsOpened': 'automaticallyMarkEqualMediaFilesAsOpened':
instance.automaticallyMarkEqualMediaFilesAsOpened, instance.automaticallyMarkEqualMediaFilesAsOpened,
'videoStabilizationEnabled': instance.videoStabilizationEnabled, 'videoStabilizationEnabled': instance.videoStabilizationEnabled,

View file

@ -263,6 +263,12 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
); );
} }
Future<void> toggleDatabaseLogging() async {
await UserService.update(
(u) => u.enableDatabaseLogging = !u.enableDatabaseLogging,
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -282,6 +288,14 @@ class _DeveloperSettingsViewState extends State<DeveloperSettingsView> {
onChanged: (_) => toggleDeveloperSettings(), onChanged: (_) => toggleDeveloperSettings(),
), ),
), ),
ListTile(
title: const Text('Enable Database Logging'),
onTap: toggleDatabaseLogging,
trailing: Switch(
value: userService.currentUser.enableDatabaseLogging,
onChanged: (_) => toggleDatabaseLogging(),
),
),
ListTile( ListTile(
title: const Text('User ID'), title: const Text('User ID'),
subtitle: Text(userService.currentUser.userId.toString()), subtitle: Text(userService.currentUser.userId.toString()),

View file

@ -3,7 +3,7 @@ description: "twonly, a privacy-friendly way to connect with friends through sec
publish_to: 'none' publish_to: 'none'
version: 0.2.23+132 version: 0.2.24+133
environment: environment:
sdk: ^3.11.0 sdk: ^3.11.0