From 97337988789ee1d3b7891ccc683e20892a7d4eda Mon Sep 17 00:00:00 2001 From: ZhuchkaTriplesix Date: Sun, 25 Oct 2026 13:19:40 +0300 Subject: [PATCH] feat(logging): Help > Open Logs Folder and Copy Diagnostics (#1760) --- lib/core/util/diagnostics_report.dart | 25 +++++++++ lib/features/help/log_actions.dart | 53 +++++++++++++++++++ .../macos/querya_platform_menu_bar.dart | 16 ++++++ lib/features/main_screen/main_screen.dart | 3 ++ .../main_screen/querya_window_title_bar.dart | 13 +++++ test/core/util/diagnostics_report_test.dart | 31 +++++++++++ 6 files changed, 141 insertions(+) create mode 100644 lib/core/util/diagnostics_report.dart create mode 100644 lib/features/help/log_actions.dart create mode 100644 test/core/util/diagnostics_report_test.dart diff --git a/lib/core/util/diagnostics_report.dart b/lib/core/util/diagnostics_report.dart new file mode 100644 index 00000000..ef05263d --- /dev/null +++ b/lib/core/util/diagnostics_report.dart @@ -0,0 +1,25 @@ +/// The text a user pastes into a bug report: what runs where, and the end of +/// the log. The log lines are already redacted by `QueryaLog`. +String buildDiagnosticsReport({ + required String appVersion, + required String operatingSystem, + required String operatingSystemVersion, + required String dartVersion, + required List logLines, + DateTime? now, +}) { + final out = StringBuffer() + ..writeln('Querya diagnostics') + ..writeln('Generated: ${(now ?? DateTime.now()).toUtc().toIso8601String()}') + ..writeln('Version: $appVersion') + ..writeln('OS: $operatingSystem $operatingSystemVersion') + ..writeln('Dart: $dartVersion') + ..writeln() + ..writeln('Last ${logLines.length} log lines:'); + if (logLines.isEmpty) { + out.writeln('(the log is empty)'); + } else { + logLines.forEach(out.writeln); + } + return out.toString(); +} diff --git a/lib/features/help/log_actions.dart b/lib/features/help/log_actions.dart new file mode 100644 index 00000000..f989c21f --- /dev/null +++ b/lib/features/help/log_actions.dart @@ -0,0 +1,53 @@ +import 'dart:io'; + +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:package_info_plus/package_info_plus.dart'; +import 'package:querya_desktop/core/extensions/sandbox/sandbox_log_paths.dart'; +import 'package:querya_desktop/core/platform/open_directory.dart'; +import 'package:querya_desktop/core/util/diagnostics_report.dart'; +import 'package:querya_desktop/core/util/querya_log.dart'; +import 'package:querya_desktop/shared/widgets/app_toast.dart'; + +/// How many log lines [copyDiagnostics] puts in the report. +const diagnosticsLogLines = 200; + +/// Help > Open logs folder. +Future openLogsFolder(BuildContext context) async { + final dir = + QueryaLog.instance.directory ?? await SandboxLogPaths.logsDirectory(); + final opened = await openDirectoryInFileManager(dir.path); + if (!opened && context.mounted) { + showAppToast( + context: context, + variant: AppToastVariant.error, + message: 'Could not open the logs folder: ${dir.path}', + ); + } +} + +/// Help > Copy diagnostics: version, OS and the end of the log to the clipboard. +Future copyDiagnostics(BuildContext context) async { + String version; + try { + final info = await PackageInfo.fromPlatform(); + version = '${info.version}+${info.buildNumber}'; + } catch (_) { + version = 'unknown'; + } + final report = buildDiagnosticsReport( + appVersion: version, + operatingSystem: Platform.operatingSystem, + operatingSystemVersion: Platform.operatingSystemVersion, + dartVersion: Platform.version.split(' ').first, + logLines: await QueryaLog.instance.tail(diagnosticsLogLines), + ); + await Clipboard.setData(ClipboardData(text: report)); + if (context.mounted) { + showAppToast( + context: context, + variant: AppToastVariant.success, + message: 'Diagnostics copied to the clipboard.', + ); + } +} diff --git a/lib/features/macos/querya_platform_menu_bar.dart b/lib/features/macos/querya_platform_menu_bar.dart index ebcf7425..20f1e53a 100644 --- a/lib/features/macos/querya_platform_menu_bar.dart +++ b/lib/features/macos/querya_platform_menu_bar.dart @@ -26,6 +26,8 @@ class QueryaPlatformMenuBar extends StatelessWidget { this.onToggleSidebar, this.onOpenPreferences, this.onOpenWelcomeTour, + this.onOpenLogsFolder, + this.onCopyDiagnostics, this.onGoHome, this.onFocusFilterBar, this.onToggleGroupings, @@ -41,6 +43,8 @@ class QueryaPlatformMenuBar extends StatelessWidget { final VoidCallback? onToggleSidebar; final VoidCallback? onOpenPreferences; final VoidCallback? onOpenWelcomeTour; + final VoidCallback? onOpenLogsFolder; + final VoidCallback? onCopyDiagnostics; final VoidCallback? onGoHome; final VoidCallback? onFocusFilterBar; final VoidCallback? onToggleGroupings; @@ -370,6 +374,18 @@ class QueryaPlatformMenuBar extends StatelessWidget { label: 'Report an Issue...', onSelected: () => unawaited(launchUrl(Uri.parse(issuesUrl))), ), + PlatformMenuItemGroup( + members: [ + PlatformMenuItem( + label: 'Open Logs Folder', + onSelected: onOpenLogsFolder, + ), + PlatformMenuItem( + label: 'Copy Diagnostics', + onSelected: onCopyDiagnostics, + ), + ], + ), ], ), ], diff --git a/lib/features/main_screen/main_screen.dart b/lib/features/main_screen/main_screen.dart index f661894c..30c0d718 100644 --- a/lib/features/main_screen/main_screen.dart +++ b/lib/features/main_screen/main_screen.dart @@ -16,6 +16,7 @@ import 'package:querya_desktop/core/storage/local_db.dart'; import 'package:querya_desktop/core/extensions/sandbox/unsandboxed_launch_consent_gate.dart'; import 'package:querya_desktop/features/extensions/presentation/widgets/unsandboxed_driver_consent_dialog.dart'; import 'package:querya_desktop/features/connections/connections_panel.dart'; +import 'package:querya_desktop/features/help/log_actions.dart'; import 'package:querya_desktop/features/macos/querya_platform_menu_bar.dart'; import 'package:querya_desktop/features/settings/preferences_dialog.dart'; import 'package:querya_desktop/shared/widgets/widgets.dart'; @@ -319,6 +320,8 @@ class _MainScreenState extends State { onToggleSidebar: () => _splitKey.currentState?.toggleSidebar(), onOpenPreferences: () => showPreferencesDialog(context), onOpenWelcomeTour: () => _dialogs.onOpenWelcomeTour(), + onOpenLogsFolder: () => unawaited(openLogsFolder(context)), + onCopyDiagnostics: () => unawaited(copyDiagnostics(context)), onGoHome: _nav.onGoHome, child: SqlEditorGlobalActions( activeConnection: workspace.activeConnection, diff --git a/lib/features/main_screen/querya_window_title_bar.dart b/lib/features/main_screen/querya_window_title_bar.dart index 2bb8fde0..2eb23b46 100644 --- a/lib/features/main_screen/querya_window_title_bar.dart +++ b/lib/features/main_screen/querya_window_title_bar.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io' show Platform; import 'package:bitsdojo_window/bitsdojo_window.dart'; @@ -14,6 +15,7 @@ import 'package:querya_desktop/features/settings/preferences_dialog.dart'; import 'package:querya_desktop/shared/widgets/connection_environment_badge.dart'; import 'package:querya_desktop/features/extensions/presentation/pages/extension_manager_dialog.dart'; import 'package:querya_desktop/features/help/about_dialog.dart'; +import 'package:querya_desktop/features/help/log_actions.dart'; import 'package:querya_desktop/features/updater/update_available_badge.dart'; import 'package:querya_desktop/features/updater/update_controller.dart'; import 'package:querya_desktop/features/updater/update_dialog.dart'; @@ -413,6 +415,17 @@ class QueryaWindowTitleBar extends StatelessWidget { QueryaMenuItem( onPressed: (_) => openQueryaDocumentation(), child: const Text('Documentation')), + const QueryaMenuDivider(), + QueryaMenuItem( + onPressed: (_) => + unawaited(openLogsFolder(titleBarContext)), + child: const Text('Open Logs Folder'), + ), + QueryaMenuItem( + onPressed: (_) => + unawaited(copyDiagnostics(titleBarContext)), + child: const Text('Copy Diagnostics'), + ), ], child: const Text('Help'), ), diff --git a/test/core/util/diagnostics_report_test.dart b/test/core/util/diagnostics_report_test.dart new file mode 100644 index 00000000..601e907a --- /dev/null +++ b/test/core/util/diagnostics_report_test.dart @@ -0,0 +1,31 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:querya_desktop/core/util/diagnostics_report.dart'; + +void main() { + test('the report names the version, the OS and carries the log lines', () { + final text = buildDiagnosticsReport( + appVersion: '0.5.0+7', + operatingSystem: 'linux', + operatingSystemVersion: '6.1', + dartVersion: '3.9.0', + logLines: ['a', 'b'], + now: DateTime.utc(2026, 10, 25, 12), + ); + + expect(text, contains('Version: 0.5.0+7')); + expect(text, contains('OS: linux 6.1')); + expect(text, contains('Generated: 2026-10-25T12:00:00.000Z')); + expect(text, contains('Last 2 log lines:\na\nb\n')); + }); + + test('an empty log is said so', () { + final text = buildDiagnosticsReport( + appVersion: '1', + operatingSystem: 'linux', + operatingSystemVersion: '', + dartVersion: '3', + logLines: const [], + ); + expect(text, contains('(the log is empty)')); + }); +}