Skip to content

Commit a540919

Browse files
feat(cli_tools): Make the analytics behavior customizable (#87)
This is a backwards-compatible change that opens up the analytics configuration and sending behavior to be overridden by subclasses. <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Dynamic analytics enablement that determines and applies analytics settings at start time. * **Refactor** * Centralized analytics handling for consistent, reliable event routing across command flows and clearer documented behavior. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent 73c6b7a commit a540919

File tree

1 file changed

+57
-16
lines changed

1 file changed

+57
-16
lines changed

packages/cli_tools/lib/src/better_command_runner/better_command_runner.dart

Lines changed: 57 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,8 @@ class BetterCommandRunner<O extends OptionDefinition, T>
6666
final MessageOutput? _messageOutput;
6767
final SetLogLevel? _setLogLevel;
6868
final OnBeforeRunCommand? _onBeforeRunCommand;
69-
OnAnalyticsEvent? _onAnalyticsEvent;
69+
final OnAnalyticsEvent? _onAnalyticsEvent;
70+
bool _analyticsEnabled;
7071

7172
/// The environment variables used for configuration resolution.
7273
final Map<String, String> envVariables;
@@ -157,6 +158,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
157158
_setLogLevel = setLogLevel,
158159
_onBeforeRunCommand = onBeforeRunCommand,
159160
_onAnalyticsEvent = onAnalyticsEvent,
161+
_analyticsEnabled = onAnalyticsEvent != null,
160162
envVariables = env ?? Platform.environment,
161163
super(
162164
usageLineLength: wrapTextColumn,
@@ -167,7 +169,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
167169
_globalOptions = <O>[
168170
StandardGlobalOption.quiet as O,
169171
StandardGlobalOption.verbose as O,
170-
if (_onAnalyticsEvent != null) StandardGlobalOption.analytics as O,
172+
if (this.onAnalyticsEvent != null) StandardGlobalOption.analytics as O,
171173
];
172174
} else {
173175
throw ArgumentError(
@@ -198,7 +200,50 @@ class BetterCommandRunner<O extends OptionDefinition, T>
198200
}
199201

200202
/// Checks if analytics is enabled.
201-
bool analyticsEnabled() => _onAnalyticsEvent != null;
203+
/// Note that the return value may change after the [run] method has started.
204+
/// Can be overridden.
205+
bool analyticsEnabled() => _analyticsEnabled;
206+
207+
/// Gets the [onAnalyticsEvent] callback, if set.
208+
OnAnalyticsEvent? get onAnalyticsEvent => _onAnalyticsEvent;
209+
210+
/// Sends an analytics event, provided the analytics are enabled.
211+
/// Invoked from BetterCommandRunner upon command execution
212+
/// with the event name, or command name if applicable.
213+
/// Can be overridden to customize the event sending behavior.
214+
void sendAnalyticsEvent(final String event) {
215+
if (analyticsEnabled()) {
216+
try {
217+
onAnalyticsEvent?.call(event);
218+
} catch (_) {
219+
// Silently ignore analytics sending errors to not disrupt the main flow
220+
}
221+
}
222+
}
223+
224+
/// Determines the analytics settings based on configuration / settings.
225+
/// Called from [run] before any analytics events are sent and before any
226+
/// command is run.
227+
///
228+
/// [globalConfiguration] is set before this method is called.
229+
///
230+
/// By default it checks whether the [onAnalyticsEvent] callback is set
231+
/// and the `--analytics` option.
232+
/// Subclasses can override this method to customize the behavior,
233+
/// e.g. to ask the user for permission.
234+
Future<bool> determineAnalyticsSettings() async {
235+
if (onAnalyticsEvent == null) {
236+
return false;
237+
}
238+
239+
if (globalConfiguration.findValueOf(
240+
argName: BetterCommandRunnerFlags.analytics) ==
241+
false) {
242+
return false;
243+
}
244+
245+
return true;
246+
}
202247

203248
/// Parses [args] and invokes [Command.run] on the chosen command.
204249
///
@@ -211,11 +256,13 @@ class BetterCommandRunner<O extends OptionDefinition, T>
211256
/// the global configuration is set, see [globalConfiguration].
212257
@override
213258
Future<T?> run(final Iterable<String> args) {
214-
return Future.sync(() {
259+
return Future.sync(() async {
215260
final argResults = parse(args);
216261
globalConfiguration = resolveConfiguration(argResults);
217262

218263
try {
264+
_analyticsEnabled = await determineAnalyticsSettings();
265+
219266
if (globalConfiguration.errors.isNotEmpty) {
220267
final buffer = StringBuffer();
221268
final errors = globalConfiguration.errors.map(formatConfigError);
@@ -224,7 +271,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
224271
}
225272
} on UsageException catch (e) {
226273
messageOutput?.logUsageException(e);
227-
_onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid);
274+
sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.invalid);
228275
rethrow;
229276
}
230277

@@ -239,7 +286,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
239286
return super.parse(args);
240287
} on UsageException catch (e) {
241288
messageOutput?.logUsageException(e);
242-
_onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid);
289+
sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.invalid);
243290
rethrow;
244291
}
245292
}
@@ -268,21 +315,15 @@ class BetterCommandRunner<O extends OptionDefinition, T>
268315
commandName: topLevelResults.command?.name,
269316
);
270317

271-
if (globalConfiguration.findValueOf(
272-
argName: BetterCommandRunnerFlags.analytics) ==
273-
false) {
274-
_onAnalyticsEvent = null;
275-
}
276-
277318
unawaited(
278-
Future(() async {
319+
Future.sync(() {
279320
final command = topLevelResults.command;
280321
if (command != null) {
281322
// Command name can only be null for top level results.
282323
// But since we are taking the name of a command from the top level
283324
// results there should always be a name specified.
284325
assert(command.name != null, 'Command name should never be null.');
285-
_onAnalyticsEvent?.call(
326+
sendAnalyticsEvent(
286327
command.name ?? BetterCommandRunnerAnalyticsEvents.invalid,
287328
);
288329
return;
@@ -297,7 +338,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
297338
// so the try/catch statement can't be fully compensated for handled here.
298339
final noUnexpectedArgs = topLevelResults.rest.isEmpty;
299340
if (noUnexpectedArgs) {
300-
_onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.help);
341+
sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.help);
301342
}
302343
}),
303344
);
@@ -308,7 +349,7 @@ class BetterCommandRunner<O extends OptionDefinition, T>
308349
return await super.runCommand(topLevelResults);
309350
} on UsageException catch (e) {
310351
messageOutput?.logUsageException(e);
311-
_onAnalyticsEvent?.call(BetterCommandRunnerAnalyticsEvents.invalid);
352+
sendAnalyticsEvent(BetterCommandRunnerAnalyticsEvents.invalid);
312353
rethrow;
313354
}
314355
}

0 commit comments

Comments
 (0)