Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions lib/core/util/diagnostics_report.dart
Original file line number Diff line number Diff line change
@@ -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<String> 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();
}
53 changes: 53 additions & 0 deletions lib/features/help/log_actions.dart
Original file line number Diff line number Diff line change
@@ -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<void> 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<void> 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.',
);
}
}
16 changes: 16 additions & 0 deletions lib/features/macos/querya_platform_menu_bar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ class QueryaPlatformMenuBar extends StatelessWidget {
this.onToggleSidebar,
this.onOpenPreferences,
this.onOpenWelcomeTour,
this.onOpenLogsFolder,
this.onCopyDiagnostics,
this.onGoHome,
this.onFocusFilterBar,
this.onToggleGroupings,
Expand All @@ -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;
Expand Down Expand Up @@ -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,
),
],
),
],
),
],
Expand Down
3 changes: 3 additions & 0 deletions lib/features/main_screen/main_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -319,6 +320,8 @@ class _MainScreenState extends State<MainScreen> {
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,
Expand Down
13 changes: 13 additions & 0 deletions lib/features/main_screen/querya_window_title_bar.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'dart:async';
import 'dart:io' show Platform;

import 'package:bitsdojo_window/bitsdojo_window.dart';
Expand All @@ -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';
Expand Down Expand Up @@ -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'),
),
Expand Down
31 changes: 31 additions & 0 deletions test/core/util/diagnostics_report_test.dart
Original file line number Diff line number Diff line change
@@ -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)'));
});
}
Loading