From 161492290433d7ae4961416ae7a8b8e3b25ff22c Mon Sep 17 00:00:00 2001 From: Christer Date: Tue, 28 Oct 2025 12:25:06 +0100 Subject: [PATCH 1/3] feat(cli_tools): Make the analytics behavior customizable --- .../better_command_runner.dart | 64 ++++++++++++++----- 1 file changed, 49 insertions(+), 15 deletions(-) diff --git a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart index 7f62c4c..0f497d3 100644 --- a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart +++ b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart @@ -66,7 +66,8 @@ class BetterCommandRunner final MessageOutput? _messageOutput; final SetLogLevel? _setLogLevel; final OnBeforeRunCommand? _onBeforeRunCommand; - OnAnalyticsEvent? _onAnalyticsEvent; + final OnAnalyticsEvent? _onAnalyticsEvent; + bool _analyticsEnabled; /// The environment variables used for configuration resolution. final Map envVariables; @@ -157,6 +158,7 @@ class BetterCommandRunner _setLogLevel = setLogLevel, _onBeforeRunCommand = onBeforeRunCommand, _onAnalyticsEvent = onAnalyticsEvent, + _analyticsEnabled = onAnalyticsEvent != null, envVariables = env ?? Platform.environment, super( usageLineLength: wrapTextColumn, @@ -198,7 +200,43 @@ class BetterCommandRunner } /// Checks if analytics is enabled. - bool analyticsEnabled() => _onAnalyticsEvent != null; + /// Note that the return value may change after the [run] method has started. + /// Can be overridden. + bool analyticsEnabled() => _analyticsEnabled; + + /// Sends an analytics event, provided the analytics are enabled. + /// Invoked from BetterCommandRunner upon command execution + /// with the event name, or command name if applicable. + /// Can be overridden to customize the event sending behavior. + void sendAnalyticsEvent(final String event) { + if (analyticsEnabled()) { + _onAnalyticsEvent?.call(event); + } + } + + /// Determines the analytics settings based on configuration / settings. + /// Called from [run] before any analytics events are sent and before any + /// command is run. + /// + /// [globalConfiguration] is set before this method is called. + /// + /// By default it checks whether the [onAnalyticsEvent] callback is set + /// and the `--analytics` option. + /// Subclasses can override this method to customize the behavior, + /// e.g. to ask the user for permission. + Future determineAnalyticsSettings() async { + if (_onAnalyticsEvent == null) { + return false; + } + + if (globalConfiguration.findValueOf( + argName: BetterCommandRunnerFlags.analytics) == + false) { + return false; + } + + return true; + } /// Parses [args] and invokes [Command.run] on the chosen command. /// @@ -211,11 +249,13 @@ class BetterCommandRunner /// the global configuration is set, see [globalConfiguration]. @override Future run(final Iterable args) { - return Future.sync(() { + return Future.sync(() async { final argResults = parse(args); globalConfiguration = resolveConfiguration(argResults); try { + _analyticsEnabled = await determineAnalyticsSettings(); + if (globalConfiguration.errors.isNotEmpty) { final buffer = StringBuffer(); final errors = globalConfiguration.errors.map(formatConfigError); @@ -224,7 +264,7 @@ class BetterCommandRunner } } on UsageException catch (e) { messageOutput?.logUsageException(e); - _onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid); + sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.invalid); rethrow; } @@ -239,7 +279,7 @@ class BetterCommandRunner return super.parse(args); } on UsageException catch (e) { messageOutput?.logUsageException(e); - _onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid); + sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.invalid); rethrow; } } @@ -268,21 +308,15 @@ class BetterCommandRunner commandName: topLevelResults.command?.name, ); - if (globalConfiguration.findValueOf( - argName: BetterCommandRunnerFlags.analytics) == - false) { - _onAnalyticsEvent = null; - } - unawaited( - Future(() async { + Future.sync(() { final command = topLevelResults.command; if (command != null) { // Command name can only be null for top level results. // But since we are taking the name of a command from the top level // results there should always be a name specified. assert(command.name != null, 'Command name should never be null.'); - _onAnalyticsEvent?.call( + sendAnalyticsEvent( command.name ?? BetterCommandRunnerAnalyticsEvents.invalid, ); return; @@ -297,7 +331,7 @@ class BetterCommandRunner // so the try/catch statement can't be fully compensated for handled here. final noUnexpectedArgs = topLevelResults.rest.isEmpty; if (noUnexpectedArgs) { - _onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.help); + sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.help); } }), ); @@ -308,7 +342,7 @@ class BetterCommandRunner return await super.runCommand(topLevelResults); } on UsageException catch (e) { messageOutput?.logUsageException(e); - _onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid); + sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.invalid); rethrow; } } From 2f94b7f6a2fe15329dd1e43e138dfa7ed9e7330b Mon Sep 17 00:00:00 2001 From: Christer Date: Tue, 28 Oct 2025 13:08:41 +0100 Subject: [PATCH 2/3] feat(cli_tools): Make onAnalyticsEvent a public getter --- .../src/better_command_runner/better_command_runner.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart index 0f497d3..3b65a70 100644 --- a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart +++ b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart @@ -169,7 +169,7 @@ class BetterCommandRunner _globalOptions = [ StandardGlobalOption.quiet as O, StandardGlobalOption.verbose as O, - if (_onAnalyticsEvent != null) StandardGlobalOption.analytics as O, + if (this.onAnalyticsEvent != null) StandardGlobalOption.analytics as O, ]; } else { throw ArgumentError( @@ -204,13 +204,16 @@ class BetterCommandRunner /// Can be overridden. bool analyticsEnabled() => _analyticsEnabled; + /// Gets the [onAnalyticsEvent] callback, if set. + OnAnalyticsEvent? get onAnalyticsEvent => _onAnalyticsEvent; + /// Sends an analytics event, provided the analytics are enabled. /// Invoked from BetterCommandRunner upon command execution /// with the event name, or command name if applicable. /// Can be overridden to customize the event sending behavior. void sendAnalyticsEvent(final String event) { if (analyticsEnabled()) { - _onAnalyticsEvent?.call(event); + onAnalyticsEvent?.call(event); } } @@ -225,7 +228,7 @@ class BetterCommandRunner /// Subclasses can override this method to customize the behavior, /// e.g. to ask the user for permission. Future determineAnalyticsSettings() async { - if (_onAnalyticsEvent == null) { + if (onAnalyticsEvent == null) { return false; } From 4cff50feaada938d4a36ec2b05cff5a60d2fa4a9 Mon Sep 17 00:00:00 2001 From: Christer Date: Tue, 28 Oct 2025 13:59:14 +0100 Subject: [PATCH 3/3] fix(cli_tools): Exceptions from analytics send no longer propagate --- .../src/better_command_runner/better_command_runner.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart index 3b65a70..4e31369 100644 --- a/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart +++ b/packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart @@ -213,7 +213,11 @@ class BetterCommandRunner /// Can be overridden to customize the event sending behavior. void sendAnalyticsEvent(final String event) { if (analyticsEnabled()) { - onAnalyticsEvent?.call(event); + try { + onAnalyticsEvent?.call(event); + } catch (_) { + // Silently ignore analytics sending errors to not disrupt the main flow + } } }