From 0b553fc49f1bd804e48d76d3a0cb95600829fe02 Mon Sep 17 00:00:00 2001 From: Fernando Ferrara Date: Thu, 30 Nov 2023 01:51:37 -0300 Subject: [PATCH] fix: replace Google Analytics with PostHog (#42) --- bin/stacked.dart | 12 +- .../commands/create/create_app_command.dart | 9 +- .../create/create_bottom_sheet_command.dart | 9 +- .../create/create_dialog_command.dart | 9 +- .../create/create_service_command.dart | 9 +- .../commands/create/create_view_command.dart | 9 +- .../create/create_widget_command.dart | 9 +- .../delete/delete_dialog_command.dart | 9 +- .../delete/delete_service_command.dart | 9 +- .../commands/delete/delete_view_command.dart | 9 +- .../commands/generate/generate_command.dart | 8 +- lib/src/commands/update/update_command.dart | 6 +- .../posthog_api_key_not_found_exception.dart | 12 + lib/src/locator.dart | 7 +- lib/src/services/analytics_service.dart | 197 --------------- lib/src/services/config_service.dart | 17 +- lib/src/services/posthog_service.dart | 238 ++++++++++++++++++ lib/src/services/process_service.dart | 7 +- pubspec.yaml | 3 +- test/helpers/test_helpers.dart | 14 +- test/helpers/test_helpers.mocks.dart | 146 ++++++++--- 21 files changed, 448 insertions(+), 300 deletions(-) create mode 100644 lib/src/exceptions/posthog_api_key_not_found_exception.dart delete mode 100644 lib/src/services/analytics_service.dart create mode 100644 lib/src/services/posthog_service.dart diff --git a/bin/stacked.dart b/bin/stacked.dart index 49e4fe2..6e41afc 100644 --- a/bin/stacked.dart +++ b/bin/stacked.dart @@ -11,7 +11,7 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/exceptions/invalid_stacked_structure_exception.dart'; import 'package:stacked_cli/src/locator.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/pub_service.dart'; Future main(List arguments) async { @@ -59,14 +59,14 @@ Future main(List arguments) async { runner.run(arguments); } on InvalidStackedStructureException catch (e) { stdout.writeln(e.message); - locator().logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: e.toString(), ); exit(2); } catch (e, s) { stdout.writeln(e.toString()); - locator().logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: e.toString(), stackTrace: s.toString(), @@ -83,12 +83,12 @@ Future _handleVersion() async { /// Enables or disables sending of analytics data. bool _handleAnalytics(ArgResults argResults) { if (argResults[ksEnableAnalytics]) { - locator().enable(true); + locator().enable(true); return true; } if (argResults[ksDisableAnalytics]) { - locator().enable(false); + locator().enable(false); return true; } @@ -97,7 +97,7 @@ bool _handleAnalytics(ArgResults argResults) { /// Allows user decide to enable or not analytics on first run. Future _handleFirstRun() async { - final analyticsService = locator(); + final analyticsService = locator(); if (!analyticsService.isFirstRun) return; stdout.writeln(''' diff --git a/lib/src/commands/create/create_app_command.dart b/lib/src/commands/create/create_app_command.dart index c71d31f..7c170ca 100644 --- a/lib/src/commands/create/create_app_command.dart +++ b/lib/src/commands/create/create_app_command.dart @@ -6,10 +6,10 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/config_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; import 'package:stacked_cli/src/templates/compiled_constants.dart'; @@ -17,7 +17,7 @@ import 'package:stacked_cli/src/templates/template_constants.dart'; import 'package:stacked_cli/src/templates/template_helper.dart'; class CreateAppCommand extends Command { - final _analyticsService = locator(); + final _analyticsService = locator(); final _configService = locator(); final _fileService = locator(); final _log = locator(); @@ -112,7 +112,10 @@ class CreateAppCommand extends Command { await _processService.runBuildRunner(workingDirectory: workingDirectory); await _processService.runFormat(appName: workingDirectory); await _clean(workingDirectory: workingDirectory); - unawaited(_analyticsService.createAppEvent(name: appName)); + await _analyticsService.createAppEvent( + name: appName, + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/create/create_bottom_sheet_command.dart b/lib/src/commands/create/create_bottom_sheet_command.dart index 7854101..9a30415 100644 --- a/lib/src/commands/create/create_bottom_sheet_command.dart +++ b/lib/src/commands/create/create_bottom_sheet_command.dart @@ -5,9 +5,9 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -20,7 +20,7 @@ class CreateBottomSheetCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -87,8 +87,9 @@ class CreateBottomSheetCommand extends Command with ProjectStructureValidator { ); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited( - _analyticsService.createBottomSheetEvent(name: bottomSheetName), + await _analyticsService.createBottomSheetEvent( + name: bottomSheetName, + arguments: argResults!.arguments, ); } catch (e, s) { _log.error(message: e.toString()); diff --git a/lib/src/commands/create/create_dialog_command.dart b/lib/src/commands/create/create_dialog_command.dart index e4cb169..0c4c590 100644 --- a/lib/src/commands/create/create_dialog_command.dart +++ b/lib/src/commands/create/create_dialog_command.dart @@ -5,9 +5,9 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -20,7 +20,7 @@ class CreateDialogCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -87,7 +87,10 @@ class CreateDialogCommand extends Command with ProjectStructureValidator { ); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited(_analyticsService.createDialogEvent(name: dialogName)); + await _analyticsService.createDialogEvent( + name: dialogName, + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/create/create_service_command.dart b/lib/src/commands/create/create_service_command.dart index 3fcdeaa..84278a6 100644 --- a/lib/src/commands/create/create_service_command.dart +++ b/lib/src/commands/create/create_service_command.dart @@ -5,9 +5,9 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -20,7 +20,7 @@ class CreateServiceCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -80,7 +80,10 @@ class CreateServiceCommand extends Command with ProjectStructureValidator { templateType: templateType, ); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited(_analyticsService.createServiceEvent(name: serviceName)); + await _analyticsService.createServiceEvent( + name: serviceName, + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/create/create_view_command.dart b/lib/src/commands/create/create_view_command.dart index 0e34c66..6b7240b 100644 --- a/lib/src/commands/create/create_view_command.dart +++ b/lib/src/commands/create/create_view_command.dart @@ -5,9 +5,9 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -20,7 +20,7 @@ class CreateViewCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -93,7 +93,10 @@ class CreateViewCommand extends Command with ProjectStructureValidator { templateType: templateType, ); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited(_analyticsService.createViewEvent(name: viewName)); + await _analyticsService.createViewEvent( + name: viewName, + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/create/create_widget_command.dart b/lib/src/commands/create/create_widget_command.dart index a2c17ae..bf01fea 100644 --- a/lib/src/commands/create/create_widget_command.dart +++ b/lib/src/commands/create/create_widget_command.dart @@ -5,9 +5,9 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -20,7 +20,7 @@ class CreateWidgetCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => 'Creates a widget with their model file.'; @@ -89,7 +89,10 @@ class CreateWidgetCommand extends Command with ProjectStructureValidator { templateType: templateType, ); - unawaited(_analyticsService.createWidgetEvent(name: widgetName)); + await _analyticsService.createWidgetEvent( + name: widgetName, + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/delete/delete_dialog_command.dart b/lib/src/commands/delete/delete_dialog_command.dart index 925d13c..b559070 100644 --- a/lib/src/commands/delete/delete_dialog_command.dart +++ b/lib/src/commands/delete/delete_dialog_command.dart @@ -6,10 +6,10 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -23,7 +23,7 @@ class DeleteDialogCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -69,8 +69,9 @@ class DeleteDialogCommand extends Command with ProjectStructureValidator { await _removeDialogFromDependency( outputPath: workingDirectory, dialogName: dialogName); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited( - _analyticsService.deleteDialogEvent(name: argResults!.rest.first), + await _analyticsService.deleteDialogEvent( + name: argResults!.rest.first, + arguments: argResults!.arguments, ); } on PathNotFoundException catch (e) { _log.error(message: e.toString()); diff --git a/lib/src/commands/delete/delete_service_command.dart b/lib/src/commands/delete/delete_service_command.dart index 9733a14..6576479 100644 --- a/lib/src/commands/delete/delete_service_command.dart +++ b/lib/src/commands/delete/delete_service_command.dart @@ -5,10 +5,10 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -22,7 +22,7 @@ class DeleteServiceCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -71,9 +71,10 @@ class DeleteServiceCommand extends Command with ProjectStructureValidator { await _removeServiceFromDependency( outputPath: workingDirectory, serviceName: serviceName); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited(_analyticsService.deleteServiceEvent( + await _analyticsService.deleteServiceEvent( name: argResults!.rest.first, - )); + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/delete/delete_view_command.dart b/lib/src/commands/delete/delete_view_command.dart index fad09d1..1d86b78 100644 --- a/lib/src/commands/delete/delete_view_command.dart +++ b/lib/src/commands/delete/delete_view_command.dart @@ -5,10 +5,10 @@ import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/mixins/project_structure_validator_mixin.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; import 'package:stacked_cli/src/services/template_service.dart'; @@ -22,7 +22,7 @@ class DeleteViewCommand extends Command with ProjectStructureValidator { final _processService = locator(); final _pubspecService = locator(); final _templateService = locator(); - final _analyticsService = locator(); + final _analyticsService = locator(); @override String get description => @@ -69,8 +69,9 @@ class DeleteViewCommand extends Command with ProjectStructureValidator { await _removeViewFromRoute( outputPath: workingDirectory, viewName: viewName); await _processService.runBuildRunner(workingDirectory: workingDirectory); - unawaited( - _analyticsService.deleteViewEvent(name: argResults!.rest.first), + await _analyticsService.deleteViewEvent( + name: argResults!.rest.first, + arguments: argResults!.arguments, ); } catch (e, s) { _log.error(message: e.toString()); diff --git a/lib/src/commands/generate/generate_command.dart b/lib/src/commands/generate/generate_command.dart index 4277473..0282198 100644 --- a/lib/src/commands/generate/generate_command.dart +++ b/lib/src/commands/generate/generate_command.dart @@ -4,13 +4,13 @@ import 'package:args/command_runner.dart'; import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/locator.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/templates/template_constants.dart'; class GenerateCommand extends Command { - final _analyticsService = locator(); + final _analyticsService = locator(); final _log = locator(); final _processService = locator(); @@ -45,7 +45,9 @@ class GenerateCommand extends Command { shouldDeleteConflictingOutputs: argResults?[ksDeleteConflictOutputs], shouldWatch: argResults?[ksWatch], ); - unawaited(_analyticsService.generateCodeEvent()); + await _analyticsService.generateCodeEvent( + arguments: argResults!.arguments, + ); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/commands/update/update_command.dart b/lib/src/commands/update/update_command.dart index 2818ae8..e5d93cf 100644 --- a/lib/src/commands/update/update_command.dart +++ b/lib/src/commands/update/update_command.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:args/command_runner.dart'; import 'package:stacked_cli/src/locator.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pub_service.dart'; class UpdateCommand extends Command { - final _analyticsService = locator(); + final _analyticsService = locator(); final _log = locator(); final _processService = locator(); final _pubService = locator(); @@ -25,7 +25,7 @@ class UpdateCommand extends Command { if (await _pubService.hasLatestVersion()) return; await _processService.runPubGlobalActivate(); - unawaited(_analyticsService.updateCliEvent()); + await _analyticsService.updateCliEvent(); } catch (e, s) { _log.error(message: e.toString()); unawaited(_analyticsService.logExceptionEvent( diff --git a/lib/src/exceptions/posthog_api_key_not_found_exception.dart b/lib/src/exceptions/posthog_api_key_not_found_exception.dart new file mode 100644 index 0000000..da01e93 --- /dev/null +++ b/lib/src/exceptions/posthog_api_key_not_found_exception.dart @@ -0,0 +1,12 @@ +class PostHogApiKeyNotFoundException implements Exception { + final String? message; + + const PostHogApiKeyNotFoundException({this.message}); + + @override + String toString() { + final extra = message != null ? ' $message' : ''; + + return 'PostHog API key not found!$extra'; + } +} diff --git a/lib/src/locator.dart b/lib/src/locator.dart index bb3e6ec..07c99ed 100644 --- a/lib/src/locator.dart +++ b/lib/src/locator.dart @@ -1,9 +1,9 @@ import 'package:get_it/get_it.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; import 'package:stacked_cli/src/services/path_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pub_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; @@ -21,6 +21,9 @@ Future setupLocator() async { locator.registerLazySingleton(() => TemplateHelper()); locator.registerLazySingleton(() => ProcessService()); locator.registerLazySingleton(() => ConfigService()); - locator.registerLazySingleton(() => AnalyticsService()); locator.registerLazySingleton(() => PubService()); + + final posthogService = PosthogService(); + await posthogService.init(); + locator.registerSingleton(posthogService); } diff --git a/lib/src/services/analytics_service.dart b/lib/src/services/analytics_service.dart deleted file mode 100644 index df2abbb..0000000 --- a/lib/src/services/analytics_service.dart +++ /dev/null @@ -1,197 +0,0 @@ -import 'package:stacked_cli/src/locator.dart'; -import 'package:usage/usage_io.dart'; - -import 'pub_service.dart'; - -/// Provides functionality to interact with Google Analytics -class AnalyticsService { - // Custom Dimension [1] - Version - static const String kcdVersion = 'cd1'; - - // Custom Dimension [2] - Name - static const String kcdName = 'cd2'; - - final AnalyticsIO _analytics = AnalyticsIO( - 'UA-41171112-5', - 'stacked-cli', - 'stacked_cli', - ); - - /// Is this the first time the tool has run? - bool get isFirstRun => _analytics.firstRun; - - /// Will analytics data be sent? - bool get enabled => _analytics.enabled; - - /// Enables or disables sending of analytics data. - void enable(bool value) { - _analytics.enabled = value; - } - - /// This will wait until all outstanding analytics requests have completed, - /// or until the specified duration has elapsed. - Future _waitLastPingOrCloseAtTimeout() async { - await _analytics.waitForLastPing( - timeout: const Duration(milliseconds: 200), - ); - } - - /// Sends create app command event - Future createAppEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'create', - label: 'app', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends create bottom sheet command event - Future createBottomSheetEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'create', - label: 'bottom_sheet', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends create dialog command event - Future createDialogEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'create', - label: 'dialog', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends create service command event - Future createServiceEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'create', - label: 'service', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends create view command event - Future createViewEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'create', - label: 'view', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends create widget command event - Future createWidgetEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'create', - label: 'widget', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends delete service command event - Future deleteServiceEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'delete', - label: 'service', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends delete view command event - Future deleteViewEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'delete', - label: 'view', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends delete dialog command event - Future deleteDialogEvent({required String name}) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'delete', - label: 'dialog', - parameters: {kcdVersion: version, kcdName: name}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends generate command event - Future generateCodeEvent() async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'generate', - parameters: {kcdVersion: version}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends update command event - Future updateCliEvent() async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - 'command', - 'update', - parameters: {kcdVersion: version}, - ); - await _waitLastPingOrCloseAtTimeout(); - } - - /// Sends exception event - Future logExceptionEvent({ - Level level = Level.error, - required String runtimeType, - required String message, - String stackTrace = 'Not Available', - }) async { - final version = await locator().getCurrentVersion(); - await _analytics.sendEvent( - level.toString(), - '[$runtimeType] $message', - label: 'StackTrace:\n$stackTrace', - parameters: {kcdVersion: version}, - ); - await _waitLastPingOrCloseAtTimeout(); - } -} - -enum Level { - debug, - info, - warning, - error; - - @override - String toString() { - return 'LOG::${name.toUpperCase()}'; - } -} diff --git a/lib/src/services/config_service.dart b/lib/src/services/config_service.dart index 470dbb8..32c4f18 100644 --- a/lib/src/services/config_service.dart +++ b/lib/src/services/config_service.dart @@ -6,15 +6,14 @@ import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/exceptions/config_file_not_found_exception.dart'; import 'package:stacked_cli/src/locator.dart'; import 'package:stacked_cli/src/models/config_model.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; import 'package:stacked_cli/src/services/path_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; /// Handles app configuration of stacked cli class ConfigService { final _log = locator(); - final _analyticsService = locator(); final _fileService = locator(); final _pathService = locator(); @@ -124,7 +123,7 @@ class ConfigService { if (e.shouldHaltCommand) rethrow; _log.warn(message: e.message); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( level: Level.warning, runtimeType: e.runtimeType.toString(), message: e.message, @@ -132,7 +131,7 @@ class ConfigService { ); } catch (e, s) { _log.error(message: e.toString()); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: e.toString(), stackTrace: s.toString(), @@ -158,7 +157,7 @@ class ConfigService { if (e.shouldHaltCommand) rethrow; _log.warn(message: e.message); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( level: Level.warning, runtimeType: e.runtimeType.toString(), message: e.message, @@ -166,7 +165,7 @@ class ConfigService { ); } catch (e, s) { _log.error(message: e.toString()); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: e.toString(), stackTrace: s.toString(), @@ -253,7 +252,7 @@ class ConfigService { if (e.shouldHaltCommand) rethrow; _log.warn(message: e.message); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( level: Level.warning, runtimeType: e.runtimeType.toString(), message: e.message, @@ -261,7 +260,7 @@ class ConfigService { ); } on FormatException catch (e, s) { _log.warn(message: kConfigFileMalformed); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( level: Level.warning, runtimeType: e.runtimeType.toString(), message: e.message, @@ -269,7 +268,7 @@ class ConfigService { ); } catch (e, s) { _log.error(message: e.toString()); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: e.toString(), stackTrace: s.toString(), diff --git a/lib/src/services/posthog_service.dart b/lib/src/services/posthog_service.dart new file mode 100644 index 0000000..a4e8301 --- /dev/null +++ b/lib/src/services/posthog_service.dart @@ -0,0 +1,238 @@ +import 'dart:convert'; + +import 'package:hive/hive.dart'; +import 'package:http/http.dart' as http; +import 'package:stacked_cli/src/exceptions/posthog_api_key_not_found_exception.dart'; +import 'package:stacked_cli/src/locator.dart'; + +import 'colorized_log_service.dart'; +import 'path_service.dart'; +import 'pub_service.dart'; + +const String _apiKey = String.fromEnvironment('POSTHOG_API_KEY'); + +const String _isFirstRunKey = 'isAnalyticsFirstRun'; +const String _isEnabledKey = 'isAnalyticsEnabled'; + +class PosthogService { + final bool verbose; + PosthogService({this.verbose = false}); + + final _log = locator(); + final _pubService = locator(); + final _pathService = locator(); + + final _baseUri = Uri.parse('https://app.posthog.com/capture'); + + /// Is this the first time the tool has run? + bool get isFirstRun => _box.get(_isFirstRunKey, defaultValue: true); + + /// Will analytics data be sent? + bool get enabled => _box.get(_isEnabledKey, defaultValue: false); + + /// Enables or disables sending of analytics data. + Future enable(bool value) async { + await _box.put(_isFirstRunKey, false); + await _box.put(_isEnabledKey, value); + } + + late Box _box; + + Future init() async { + Hive.init('${_pathService.configHome.path}/stacked'); + + _box = await Hive.openBox( + 'telemetry_config', + compactionStrategy: (entries, deletedEntries) => deletedEntries > 50, + ); + } + + Future _capture({ + required String event, + Map properties = const {}, + }) async { + try { + if (!enabled) return; + + if (_apiKey.isEmpty) throw PostHogApiKeyNotFoundException(); + + final version = await _pubService.getCurrentVersion(); + + final response = await http.post( + _baseUri, + headers: {"Content-Type": "application/json"}, + body: jsonEncode({ + "api_key": _apiKey, + "event": event, + "properties": { + "distinct_id": "Anonymous", + ...properties, + "version": version, + } + }), + ); + + final data = jsonDecode(response.body); + + if (response.statusCode != 200) throw Exception(data); + } on PostHogApiKeyNotFoundException catch (e) { + _log.error(message: e.toString()); + } catch (e, s) { + _log.error(message: 'Error: Event could not be sent!, StackTrace:\n$s'); + } + } + + Future createAppEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Create App", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future createBottomSheetEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Create BottomSheet", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future createDialogEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Create Dialog", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future createServiceEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Create Service", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future createViewEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Create View", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future createWidgetEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Create Widget", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future deleteDialogEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Delete Dialog", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future deleteServiceEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Delete Service", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future deleteViewEvent({ + required String name, + required List arguments, + }) async { + await _capture( + event: "Delete View", + properties: { + "name": name, + "arguments": arguments, + }, + ); + } + + Future generateCodeEvent({required List arguments}) async { + await _capture(event: "Generate Code", properties: { + "arguments": arguments, + }); + } + + Future logExceptionEvent({ + Level level = Level.error, + required String runtimeType, + required String message, + String stackTrace = 'Not Available', + }) async { + await _capture( + event: level.toString(), + properties: { + "message": '[$runtimeType] $message', + "stackTrace": stackTrace, + }, + ); + } + + Future updateCliEvent() async { + await _capture(event: "Update CLI"); + } +} + +enum Level { + debug, + info, + warning, + error; + + @override + String toString() { + return 'LOG::${name.toUpperCase()}'; + } +} diff --git a/lib/src/services/process_service.dart b/lib/src/services/process_service.dart index 7b57acb..a400ed3 100644 --- a/lib/src/services/process_service.dart +++ b/lib/src/services/process_service.dart @@ -3,13 +3,12 @@ import 'dart:io'; import 'package:stacked_cli/src/constants/command_constants.dart'; import 'package:stacked_cli/src/locator.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; /// helper service to run flutter commands class ProcessService { - final _analyticsService = locator(); final _cLog = locator(); final _configService = locator(); @@ -188,7 +187,7 @@ class ProcessService { final message = 'Command failed. Command executed: $programName ${arguments.join(' ')}\nException: ${e.message}'; _cLog.error(message: message); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: message, stackTrace: s.toString(), @@ -197,7 +196,7 @@ class ProcessService { final message = 'Command failed. Command executed: $programName ${arguments.join(' ')}\nException: ${e.toString()}'; _cLog.error(message: message); - _analyticsService.logExceptionEvent( + locator().logExceptionEvent( runtimeType: e.runtimeType.toString(), message: message, stackTrace: s.toString(), diff --git a/pubspec.yaml b/pubspec.yaml index f1afeed..c14d65e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -19,8 +19,9 @@ dependencies: json_annotation: ^4.8.1 ansicolor: ^2.0.1 xdg_directories: ^1.0.0 - usage: ^4.1.1 pub_updater: ^0.4.0 + http: ^1.1.2 + hive: ^2.2.3 dev_dependencies: lints: ^3.0.0 diff --git a/test/helpers/test_helpers.dart b/test/helpers/test_helpers.dart index 96b5af5..6f66e7d 100644 --- a/test/helpers/test_helpers.dart +++ b/test/helpers/test_helpers.dart @@ -5,11 +5,11 @@ import 'package:mockito/mockito.dart'; import 'package:stacked_cli/src/constants/message_constants.dart'; import 'package:stacked_cli/src/exceptions/config_file_not_found_exception.dart'; import 'package:stacked_cli/src/locator.dart'; -import 'package:stacked_cli/src/services/analytics_service.dart'; import 'package:stacked_cli/src/services/colorized_log_service.dart'; import 'package:stacked_cli/src/services/config_service.dart'; import 'package:stacked_cli/src/services/file_service.dart'; import 'package:stacked_cli/src/services/path_service.dart'; +import 'package:stacked_cli/src/services/posthog_service.dart'; import 'package:stacked_cli/src/services/process_service.dart'; import 'package:stacked_cli/src/services/pub_service.dart'; import 'package:stacked_cli/src/services/pubspec_service.dart'; @@ -30,7 +30,7 @@ import 'test_helpers.mocks.dart'; MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), - MockSpec(onMissingStub: OnMissingStub.returnDefault), + MockSpec(onMissingStub: OnMissingStub.returnDefault), MockSpec(onMissingStub: OnMissingStub.returnDefault), // @stacked-service-mock ]) @@ -184,10 +184,10 @@ MockConfigService getAndRegisterConfigService({ return service; } -MockAnalyticsService getAndRegisterAnalyticsService() { - _removeRegistrationIfExists(); - final service = MockAnalyticsService(); - locator.registerSingleton(service); +MockPosthogService getAndRegisterPosthogService() { + _removeRegistrationIfExists(); + final service = MockPosthogService(); + locator.registerSingleton(service); return service; } @@ -233,7 +233,7 @@ void registerServices() { getAndRegisterColorizedLogService(); getAndRegisterConfigService(); getAndRegisterProcessService(); - getAndRegisterAnalyticsService(); + getAndRegisterPosthogService(); getAndRegisterPubService(); // @stacked-mock-helper-register } diff --git a/test/helpers/test_helpers.mocks.dart b/test/helpers/test_helpers.mocks.dart index 18008b8..1ea99ea 100644 --- a/test/helpers/test_helpers.mocks.dart +++ b/test/helpers/test_helpers.mocks.dart @@ -13,11 +13,11 @@ import 'package:ansicolor/ansicolor.dart' as _i4; import 'package:mockito/mockito.dart' as _i1; import 'package:pubspec_yaml/pubspec_yaml.dart' as _i3; import 'package:stacked_cli/src/models/template_models.dart' as _i10; -import 'package:stacked_cli/src/services/analytics_service.dart' as _i17; import 'package:stacked_cli/src/services/colorized_log_service.dart' as _i14; import 'package:stacked_cli/src/services/config_service.dart' as _i15; import 'package:stacked_cli/src/services/file_service.dart' as _i5; import 'package:stacked_cli/src/services/path_service.dart' as _i8; +import 'package:stacked_cli/src/services/posthog_service.dart' as _i17; import 'package:stacked_cli/src/services/process_service.dart' as _i16; import 'package:stacked_cli/src/services/pub_service.dart' as _i18; import 'package:stacked_cli/src/services/pubspec_service.dart' as _i13; @@ -1264,10 +1264,16 @@ class MockProcessService extends _i1.Mock implements _i16.ProcessService { ); } -/// A class which mocks [AnalyticsService]. +/// A class which mocks [PosthogService]. /// /// See the documentation for Mockito's code generation for more information. -class MockAnalyticsService extends _i1.Mock implements _i17.AnalyticsService { +class MockPosthogService extends _i1.Mock implements _i17.PosthogService { + @override + bool get verbose => (super.noSuchMethod( + Invocation.getter(#verbose), + returnValue: false, + returnValueForMissingStub: false, + ) as bool); @override bool get isFirstRun => (super.noSuchMethod( Invocation.getter(#isFirstRun), @@ -1281,126 +1287,183 @@ class MockAnalyticsService extends _i1.Mock implements _i17.AnalyticsService { returnValueForMissingStub: false, ) as bool); @override - void enable(bool? value) => super.noSuchMethod( + _i6.Future enable(bool? value) => (super.noSuchMethod( Invocation.method( #enable, [value], ), - returnValueForMissingStub: null, - ); + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); + @override + _i6.Future init() => (super.noSuchMethod( + Invocation.method( + #init, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); @override - _i6.Future createAppEvent({required String? name}) => + _i6.Future createAppEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( #createAppEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future createBottomSheetEvent({required String? name}) => + _i6.Future createBottomSheetEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( #createBottomSheetEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future createDialogEvent({required String? name}) => + _i6.Future createDialogEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( #createDialogEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future createServiceEvent({required String? name}) => + _i6.Future createServiceEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( #createServiceEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future createViewEvent({required String? name}) => + _i6.Future createViewEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( #createViewEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future createWidgetEvent({required String? name}) => + _i6.Future createWidgetEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( #createWidgetEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future deleteServiceEvent({required String? name}) => + _i6.Future deleteDialogEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( - #deleteServiceEvent, + #deleteDialogEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future deleteViewEvent({required String? name}) => + _i6.Future deleteServiceEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( - #deleteViewEvent, + #deleteServiceEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future deleteDialogEvent({required String? name}) => + _i6.Future deleteViewEvent({ + required String? name, + required List? arguments, + }) => (super.noSuchMethod( Invocation.method( - #deleteDialogEvent, + #deleteViewEvent, [], - {#name: name}, + { + #name: name, + #arguments: arguments, + }, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); @override - _i6.Future generateCodeEvent() => (super.noSuchMethod( + _i6.Future generateCodeEvent({required List? arguments}) => + (super.noSuchMethod( Invocation.method( #generateCodeEvent, [], - ), - returnValue: _i6.Future.value(), - returnValueForMissingStub: _i6.Future.value(), - ) as _i6.Future); - @override - _i6.Future updateCliEvent() => (super.noSuchMethod( - Invocation.method( - #updateCliEvent, - [], + {#arguments: arguments}, ), returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), @@ -1426,6 +1489,15 @@ class MockAnalyticsService extends _i1.Mock implements _i17.AnalyticsService { returnValue: _i6.Future.value(), returnValueForMissingStub: _i6.Future.value(), ) as _i6.Future); + @override + _i6.Future updateCliEvent() => (super.noSuchMethod( + Invocation.method( + #updateCliEvent, + [], + ), + returnValue: _i6.Future.value(), + returnValueForMissingStub: _i6.Future.value(), + ) as _i6.Future); } /// A class which mocks [PubService].