From 660a29e6a84346eeb90e13adb60e1259d14d2c1f Mon Sep 17 00:00:00 2001 From: Jaemin Jo <44039707+91jaeminjo@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:37:48 -0500 Subject: [PATCH 1/2] refactor: route auth/room/shared logging through soliplex_logging Replace debugPrint in auth, room, and shared modules with structured soliplex_logging Loggers (.warning/.error with error and stackTrace). Add installLogSinks, which registers a ConsoleSink and a StdoutSink and sets the level floor (warning in release, info otherwise), wired in from main. Co-Authored-By: Claude Opus 4.8 (1M context) --- lib/main.dart | 5 +++ lib/src/core/log_sinks.dart | 32 +++++++++++++++++++ lib/src/modules/auth/connect_flow.dart | 19 ++++++++--- .../modules/auth/secure_server_storage.dart | 11 +++++-- lib/src/modules/auth/server_manager.dart | 26 +++++++++++---- .../modules/room/ui/async_action_dialog.dart | 8 +++-- .../room/ui/room_info/features_card.dart | 10 +++--- lib/src/shared/copy_button.dart | 10 +++--- test/core/log_sinks_test.dart | 19 +++++++++++ 9 files changed, 115 insertions(+), 25 deletions(-) create mode 100644 lib/src/core/log_sinks.dart create mode 100644 test/core/log_sinks_test.dart diff --git a/lib/main.dart b/lib/main.dart index 68057563..cb7d5f58 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,15 @@ import 'package:flutter/widgets.dart'; +import 'package:soliplex_logging/soliplex_logging.dart'; import 'package:soliplex_frontend/soliplex_frontend.dart'; import 'package:soliplex_frontend/flavors.dart'; +import 'package:soliplex_frontend/src/core/log_sinks.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + // See installLogSinks for where records surface (DevTools Logging vs stdout), + // what can observe each, and the per-mode level floor. + installLogSinks(LogManager.instance); final callbackParams = CallbackParamsCapture.captureNow(); clearCallbackUrl(); runSoliplexShell(await standard(callbackParams: callbackParams)); diff --git a/lib/src/core/log_sinks.dart b/lib/src/core/log_sinks.dart new file mode 100644 index 00000000..20a9aa1f --- /dev/null +++ b/lib/src/core/log_sinks.dart @@ -0,0 +1,32 @@ +import 'package:flutter/foundation.dart' show kReleaseMode; +import 'package:soliplex_logging/soliplex_logging.dart'; + +/// Registers the app's log sinks and sets the level floor. +/// +/// Two sinks, each on its own transport: +/// +/// - [ConsoleSink] goes through `dart:developer` to the VM-service logging +/// stream, so it reaches whatever client is attached — the DevTools +/// "Logging" view or an IDE debugger. On a native release build it is +/// effectively inert (AOT runs no VM service); on web it also writes the +/// browser console, which is present even in a release build. +/// - [StdoutSink] writes raw stdout via `dart:io`, which goes to whoever owns +/// the process: a terminal under `flutter run` or the launching tool under +/// `flutter run --machine`. A packaged GUI app has no attached terminal, so +/// its stdout is typically discarded (it is not Console.app / logcat — those +/// carry the `dart:developer`/OS-logging path, not raw process stdout), and +/// on web the sink is a no-op. For a view that doesn't depend on what's +/// attached, route to a file (a disk sink, or shell redirection of stdout). +/// +/// Both register in every build mode; without a sink [LogManager] discards +/// every record. Release is held to [LogLevel.warning] so the on-device stream +/// stays quiet, while debug keeps [LogLevel.info]. Shipping logs *off* the +/// device (a backend sink) is a separate decision that needs redaction and +/// consent handling, since records can carry server URLs and error details. +/// Host apps that embed this package as a library configure their own sinks. +void installLogSinks(LogManager manager) { + manager.minimumLevel = kReleaseMode ? LogLevel.warning : LogLevel.info; + manager + ..addSink(ConsoleSink()) + ..addSink(StdoutSink()); +} diff --git a/lib/src/modules/auth/connect_flow.dart b/lib/src/modules/auth/connect_flow.dart index 3ed4d1c3..0f42e7f4 100644 --- a/lib/src/modules/auth/connect_flow.dart +++ b/lib/src/modules/auth/connect_flow.dart @@ -1,6 +1,6 @@ -import 'package:flutter/foundation.dart' show debugPrint; import 'package:soliplex_agent/soliplex_agent.dart' hide AuthException; import 'package:soliplex_agent/soliplex_agent.dart' as agent show AuthException; +import 'package:soliplex_logging/soliplex_logging.dart'; import 'auth_failure_description.dart'; import 'auth_tokens.dart'; @@ -14,6 +14,8 @@ import 'selected_server_storage.dart'; import 'server_entry.dart'; import 'server_manager.dart'; +final Logger _logger = LogManager.instance.getLogger('soliplex.connect_flow'); + /// State of the server connection flow. sealed class ConnectState { const ConnectState(); @@ -150,7 +152,7 @@ class ConnectFlow { ); } } catch (e, st) { - debugPrint('ConnectFlow.connect: $e\n$st'); + _logger.error('ConnectFlow.connect failed', error: e, stackTrace: st); if (!_isCancelled(gen)) { state.value = UrlInput( message: ConnectError('Unexpected error connecting to $url: $e'), @@ -291,8 +293,11 @@ class ConnectFlow { try { await PreAuthStateStorage.clear(); } catch (e, st) { - debugPrint( - 'ConnectFlow: post-login PreAuthState clear failed: $e\n$st'); + _logger.warning( + 'ConnectFlow: post-login PreAuthState clear failed', + error: e, + stackTrace: st, + ); } // Only clear after a successful login. If the IdP challenge was // cancelled or failed, the flag stays set so the next attempt @@ -321,7 +326,11 @@ class ConnectFlow { ); } } on Exception catch (e, st) { - debugPrint('ConnectFlow._authenticate: $e\n$st'); + _logger.error( + 'ConnectFlow._authenticate failed', + error: e, + stackTrace: st, + ); await PreAuthStateStorage.clear(); if (!_isCancelled(gen)) { state.value = UrlInput( diff --git a/lib/src/modules/auth/secure_server_storage.dart b/lib/src/modules/auth/secure_server_storage.dart index e56a2dfc..b524e49c 100644 --- a/lib/src/modules/auth/secure_server_storage.dart +++ b/lib/src/modules/auth/secure_server_storage.dart @@ -1,10 +1,13 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:soliplex_logging/soliplex_logging.dart'; import 'server_storage.dart'; +final Logger _logger = + LogManager.instance.getLogger('soliplex.secure_server_storage'); + /// Persists server sessions using platform secure storage. class SecureServerStorage implements ServerStorage { SecureServerStorage({ @@ -53,7 +56,11 @@ Map deserializeStorageEntries( final json = jsonDecode(entry.value) as Map; result[serverId] = PersistedServer.fromJson(json); } catch (e, st) { - debugPrint('Failed to load stored session ${entry.key}: $e\n$st'); + _logger.warning( + 'Failed to load stored session ${entry.key}', + error: e, + stackTrace: st, + ); } } return result; diff --git a/lib/src/modules/auth/server_manager.dart b/lib/src/modules/auth/server_manager.dart index 2da7685b..cce64f4f 100644 --- a/lib/src/modules/auth/server_manager.dart +++ b/lib/src/modules/auth/server_manager.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:soliplex_agent/soliplex_agent.dart'; +import 'package:soliplex_logging/soliplex_logging.dart'; import '../../interfaces/auth_state.dart'; import 'auth_session.dart'; @@ -9,6 +9,8 @@ import 'auth_tokens.dart'; import 'server_entry.dart'; import 'server_storage.dart'; +final Logger _logger = LogManager.instance.getLogger('soliplex.server_manager'); + typedef HttpClientFactory = SoliplexHttpClient Function({ String? Function()? getToken, TokenRefresher? tokenRefresher, @@ -97,7 +99,7 @@ class ServerManager { } else { resolvedAlias = _uniqueAlias(serverUrl); if (alias != null) { - debugPrint( + _logger.warning( 'Alias "$alias" for $serverId collides with an existing alias; ' 'using "$resolvedAlias" instead', ); @@ -156,7 +158,11 @@ class ServerManager { _persistQueue[serverId] = (_persistQueue[serverId] ?? Future.value()) .then((_) => _storage.delete(serverId)) .catchError((Object e, StackTrace st) { - debugPrint('Failed to delete stored session for $serverId: $e\n$st'); + _logger.error( + 'Failed to delete stored session for $serverId', + error: e, + stackTrace: st, + ); }); } @@ -166,7 +172,7 @@ class ServerManager { try { stored = await _storage.loadAll(); } catch (e, st) { - debugPrint('Failed to load stored servers: $e\n$st'); + _logger.error('Failed to load stored servers', error: e, stackTrace: st); return; } _restoring = true; @@ -184,7 +190,11 @@ class ServerManager { server.auth.login(provider: provider, tokens: tokens); } } catch (e, st) { - debugPrint('Failed to restore server ${entry.key}: $e\n$st'); + _logger.warning( + 'Failed to restore server ${entry.key}', + error: e, + stackTrace: st, + ); } } } finally { @@ -234,7 +244,11 @@ class ServerManager { _persistQueue[serverId] = (_persistQueue[serverId] ?? Future.value()) .then((_) => persist()) .catchError((Object e, StackTrace st) { - debugPrint('Failed to persist session for $serverId: $e\n$st'); + _logger.error( + 'Failed to persist session for $serverId', + error: e, + stackTrace: st, + ); }); } } diff --git a/lib/src/modules/room/ui/async_action_dialog.dart b/lib/src/modules/room/ui/async_action_dialog.dart index a009592f..021cd02c 100644 --- a/lib/src/modules/room/ui/async_action_dialog.dart +++ b/lib/src/modules/room/ui/async_action_dialog.dart @@ -1,6 +1,10 @@ import 'package:flutter/foundation.dart' show kDebugMode; import 'package:flutter/material.dart'; import 'package:soliplex_design/soliplex_design.dart'; +import 'package:soliplex_logging/soliplex_logging.dart'; + +final Logger _logger = + LogManager.instance.getLogger('soliplex_frontend.async_action_dialog'); /// Dialog that runs an async action with loading/error states. /// @@ -46,8 +50,8 @@ class _AsyncActionDialogState extends State { try { await widget.onAction(); if (mounted) Navigator.pop(context); - } on Exception catch (e) { - debugPrint('${widget.title} failed: $e'); + } on Exception catch (e, st) { + _logger.warning('${widget.title} failed', error: e, stackTrace: st); if (mounted) { setState(() { _busy = false; diff --git a/lib/src/modules/room/ui/room_info/features_card.dart b/lib/src/modules/room/ui/room_info/features_card.dart index ea8e3758..882763a3 100644 --- a/lib/src/modules/room/ui/room_info/features_card.dart +++ b/lib/src/modules/room/ui/room_info/features_card.dart @@ -3,10 +3,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:soliplex_client/soliplex_client.dart' hide State; +import 'package:soliplex_logging/soliplex_logging.dart'; import 'room_info_widgets.dart'; import 'package:soliplex_design/soliplex_design.dart'; +final Logger _logger = + LogManager.instance.getLogger('soliplex_frontend.features_card'); + class FeaturesCard extends StatelessWidget { const FeaturesCard({ super.key, @@ -74,12 +78,8 @@ class _McpTokenRowState extends State { Future _copyToken(String token) async { try { await Clipboard.setData(ClipboardData(text: token)); - } on PlatformException catch (e, st) { - debugPrint('Clipboard.setData PlatformException: $e\n$st'); - _showCopyFeedback(_TokenCopyState.error); - return; } on Exception catch (e, st) { - debugPrint('Clipboard.setData failed: $e\n$st'); + _logger.warning('Clipboard.setData failed', error: e, stackTrace: st); _showCopyFeedback(_TokenCopyState.error); return; } diff --git a/lib/src/shared/copy_button.dart b/lib/src/shared/copy_button.dart index a5c29df9..0cdf3b1c 100644 --- a/lib/src/shared/copy_button.dart +++ b/lib/src/shared/copy_button.dart @@ -2,9 +2,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:soliplex_logging/soliplex_logging.dart'; import 'package:soliplex_design/soliplex_design.dart'; +final Logger _logger = + LogManager.instance.getLogger('soliplex_frontend.copy_button'); + class CopyButton extends StatefulWidget { const CopyButton({ super.key, @@ -38,12 +42,8 @@ class _CopyButtonState extends State { Future _copy() async { try { await Clipboard.setData(ClipboardData(text: widget.text)); - } on PlatformException catch (e, st) { - debugPrint('Clipboard.setData PlatformException: $e\n$st'); - _showFeedback(_CopyFeedback.error); - return; } on Exception catch (e, st) { - debugPrint('Clipboard.setData failed: $e\n$st'); + _logger.warning('Clipboard.setData failed', error: e, stackTrace: st); _showFeedback(_CopyFeedback.error); return; } diff --git a/test/core/log_sinks_test.dart b/test/core/log_sinks_test.dart new file mode 100644 index 00000000..5d820fac --- /dev/null +++ b/test/core/log_sinks_test.dart @@ -0,0 +1,19 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:soliplex_logging/soliplex_logging.dart'; + +import 'package:soliplex_frontend/src/core/log_sinks.dart'; + +void main() { + tearDown(LogManager.instance.reset); + + test('installLogSinks registers a console sink and a stdout sink', () { + installLogSinks(LogManager.instance); + + final sinks = LogManager.instance.sinks; + // Console sink → DevTools/IDE logging view (via dart:developer); stdout + // sink → terminal / platform console. Both, so logs are visible regardless + // of what's attached to the process. + expect(sinks.whereType(), hasLength(1)); + expect(sinks.whereType(), hasLength(1)); + }); +} From 9df655ce529f2541391cc72a745ea11712234de3 Mon Sep 17 00:00:00 2001 From: Jaemin Jo <44039707+91jaeminjo@users.noreply.github.com> Date: Mon, 22 Jun 2026 19:38:11 -0500 Subject: [PATCH 2/2] docs: changelog entry for logging migration Co-Authored-By: Claude Opus 4.8 (1M context) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c58e3426..933004d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ Versions follow the `version+build` scheme from `pubspec.yaml`, bumped via not just the checkbox, giving it a full-width tap target. - Auth: the consent notice terms are now selectable, so users can copy the text they're agreeing to. +- Logging: auth, room, and shared modules now log through `soliplex_logging` + instead of `debugPrint`, carrying error and stack-trace detail. The app + registers a console and a stdout sink at startup, holding release builds to + warnings and debug builds to info. ## [0.90.0+60] - 2026-06-16