From 1497f9e2aba40b44bb4bd2711c5ddcfbfe482b7f Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:47:37 -0500 Subject: [PATCH 01/28] Cleaned up `config_handler.dart` --- pkgs/unified_analytics/lib/src/analytics.dart | 5 +++++ .../unified_analytics/lib/src/config_handler.dart | 15 +++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 0a28a40c6..47c74ed90 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -395,6 +395,11 @@ class AnalyticsImpl implements Analytics { fs: fs, homeDirectory: homeDirectory, initializer: initializer, + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), ); // Check if the tool has already been onboarded, and if it diff --git a/pkgs/unified_analytics/lib/src/config_handler.dart b/pkgs/unified_analytics/lib/src/config_handler.dart index f9779dfab..c525dab47 100644 --- a/pkgs/unified_analytics/lib/src/config_handler.dart +++ b/pkgs/unified_analytics/lib/src/config_handler.dart @@ -7,9 +7,7 @@ import 'dart:convert'; import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:intl/intl.dart'; -import 'package:path/path.dart' as p; -import 'constants.dart'; import 'initializer.dart'; import 'utils.dart'; @@ -40,7 +38,7 @@ class ConfigHandler { final Map parsedTools = {}; - late DateTime configFileLastModified; + DateTime configFileLastModified; /// Reporting enabled unless specified by user bool _telemetryEnabled = true; @@ -49,15 +47,8 @@ class ConfigHandler { required this.fs, required this.homeDirectory, required this.initializer, - }) : configFile = fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kConfigFileName, - )) { - // Get the last time the file was updated and check this - // datestamp whenever the client asks for the telemetry enabled boolean - configFileLastModified = configFile.lastModifiedSync(); - + required this.configFile, + }) : configFileLastModified = configFile.lastModifiedSync() { // Call the method to parse the contents of the config file when // this class is initialized parseConfig(); From d42be714472ebfe87d374bd479226d0de91e6ccf Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:55:35 -0500 Subject: [PATCH 02/28] Cleaned up `log_handler.dart` --- pkgs/unified_analytics/lib/src/analytics.dart | 5 +++++ pkgs/unified_analytics/lib/src/log_handler.dart | 9 ++------- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 47c74ed90..e81637ace 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -457,6 +457,11 @@ class AnalyticsImpl implements Analytics { fs: fs, homeDirectory: homeDirectory, errorHandler: _errorHandler, + logFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kLogFileName, + )), ); // Initialize the session handler with the session_id diff --git a/pkgs/unified_analytics/lib/src/log_handler.dart b/pkgs/unified_analytics/lib/src/log_handler.dart index c406cce94..c4f950c46 100644 --- a/pkgs/unified_analytics/lib/src/log_handler.dart +++ b/pkgs/unified_analytics/lib/src/log_handler.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'package:clock/clock.dart'; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; import 'constants.dart'; import 'error_handler.dart'; @@ -170,12 +169,8 @@ class LogHandler { required this.fs, required this.homeDirectory, required ErrorHandler errorHandler, - }) : logFile = fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kLogFileName, - )), - _errorHandler = errorHandler; + required this.logFile, + }) : _errorHandler = errorHandler; /// Get stats from the persisted log file. /// From f9856142b0be2ab5b517c921a3494b4c2ae83371 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 6 Mar 2024 16:56:35 -0500 Subject: [PATCH 03/28] Cleaned up `session.dart` --- pkgs/unified_analytics/lib/src/analytics.dart | 5 +++++ pkgs/unified_analytics/lib/src/session.dart | 6 ++---- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index e81637ace..1fda651e5 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -435,6 +435,11 @@ class AnalyticsImpl implements Analytics { homeDirectory: homeDirectory, fs: fs, errorHandler: _errorHandler, + sessionFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kSessionFileName, + )), ); userProperty = UserProperty( session: _sessionHandler, diff --git a/pkgs/unified_analytics/lib/src/session.dart b/pkgs/unified_analytics/lib/src/session.dart index d19b749c5..503b43e99 100644 --- a/pkgs/unified_analytics/lib/src/session.dart +++ b/pkgs/unified_analytics/lib/src/session.dart @@ -6,7 +6,6 @@ import 'dart:convert'; import 'package:clock/clock.dart'; import 'package:file/file.dart'; -import 'package:path/path.dart' as p; import 'constants.dart'; import 'error_handler.dart'; @@ -25,9 +24,8 @@ class Session { required this.homeDirectory, required this.fs, required ErrorHandler errorHandler, - }) : sessionFile = fs.file(p.join( - homeDirectory.path, kDartToolDirectoryName, kSessionFileName)), - _errorHandler = errorHandler; + required this.sessionFile, + }) : _errorHandler = errorHandler; /// This will use the data parsed from the /// session file in the dart-tool directory From f86338b88ebc4f5bd99fec5c82bda299737a878a Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Wed, 6 Mar 2024 17:06:24 -0500 Subject: [PATCH 04/28] Clean up `survey_handler.dart` --- .../example/serving_surveys.dart | 6 ++++ pkgs/unified_analytics/lib/src/analytics.dart | 25 +++++++++++++-- .../lib/src/survey_handler.dart | 20 ++++++------ .../test/events_with_fake_test.dart | 9 ++++++ .../test/survey_handler_test.dart | 32 +++++++++++++++++-- 5 files changed, 76 insertions(+), 16 deletions(-) diff --git a/pkgs/unified_analytics/example/serving_surveys.dart b/pkgs/unified_analytics/example/serving_surveys.dart index 18631b32a..baacf20b8 100644 --- a/pkgs/unified_analytics/example/serving_surveys.dart +++ b/pkgs/unified_analytics/example/serving_surveys.dart @@ -5,6 +5,7 @@ import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:path/path.dart' as p; import 'package:unified_analytics/src/constants.dart'; import 'package:unified_analytics/src/enums.dart'; @@ -59,6 +60,11 @@ void main() async { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: home, fs: fs, + dismissedSurveyFile: fs.file(p.join( + home.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), initializedSurveys: [ Survey( uniqueId: 'uniqueId', diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 1fda651e5..38d470483 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -94,7 +94,15 @@ abstract class Analytics { toolsMessageVersion: kToolsMessageVersion, fs: fs, gaClient: gaClient, - surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs), + surveyHandler: SurveyHandler( + homeDirectory: homeDirectory, + fs: fs, + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), + ), enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, @@ -164,7 +172,15 @@ abstract class Analytics { toolsMessageVersion: kToolsMessageVersion, fs: fs, gaClient: gaClient, - surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs), + surveyHandler: SurveyHandler( + homeDirectory: homeDirectory, + fs: fs, + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), + ), enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, @@ -204,6 +220,11 @@ abstract class Analytics { FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), initializedSurveys: [], ), gaClient: gaClient ?? const FakeGAClient(), diff --git a/pkgs/unified_analytics/lib/src/survey_handler.dart b/pkgs/unified_analytics/lib/src/survey_handler.dart index 492560112..fb4085203 100644 --- a/pkgs/unified_analytics/lib/src/survey_handler.dart +++ b/pkgs/unified_analytics/lib/src/survey_handler.dart @@ -7,7 +7,6 @@ import 'dart:convert'; import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:http/http.dart' as http; -import 'package:path/path.dart' as p; import 'constants.dart'; import 'enums.dart'; @@ -190,16 +189,13 @@ class SurveyButton { } class SurveyHandler { - final File _dismissedSurveyFile; + final File dismissedSurveyFile; SurveyHandler({ required Directory homeDirectory, required FileSystem fs, - }) : _dismissedSurveyFile = fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kDismissedSurveyFileName, - )); + required this.dismissedSurveyFile, + }); /// Invoking this method will persist the survey's id in /// the local file with either a snooze or permanently dismissed @@ -227,7 +223,7 @@ class SurveyHandler { 'timestamp': clock.now().millisecondsSinceEpoch, }; - _dismissedSurveyFile.writeAsStringSync(jsonEncode(contents)); + dismissedSurveyFile.writeAsStringSync(jsonEncode(contents)); } /// Retrieve a list of strings for each [Survey] persisted on disk. @@ -284,15 +280,15 @@ class SurveyHandler { Map _parseJsonFile() { Map contents; try { - contents = jsonDecode(_dismissedSurveyFile.readAsStringSync()) + contents = jsonDecode(dismissedSurveyFile.readAsStringSync()) as Map; } on FormatException { Initializer.createDismissedSurveyFile( - dismissedSurveyFile: _dismissedSurveyFile); + dismissedSurveyFile: dismissedSurveyFile); contents = {}; } on FileSystemException { Initializer.createDismissedSurveyFile( - dismissedSurveyFile: _dismissedSurveyFile); + dismissedSurveyFile: dismissedSurveyFile); contents = {}; } @@ -344,6 +340,7 @@ class FakeSurveyHandler extends SurveyHandler { FakeSurveyHandler.fromList({ required super.homeDirectory, required super.fs, + required super.dismissedSurveyFile, required List initializedSurveys, }) { // We must pass the surveys from the list to the @@ -362,6 +359,7 @@ class FakeSurveyHandler extends SurveyHandler { FakeSurveyHandler.fromString({ required super.homeDirectory, required super.fs, + required super.dismissedSurveyFile, required String content, }) { final body = jsonDecode(content) as List; diff --git a/pkgs/unified_analytics/test/events_with_fake_test.dart b/pkgs/unified_analytics/test/events_with_fake_test.dart index 2a07c1656..5233d955d 100644 --- a/pkgs/unified_analytics/test/events_with_fake_test.dart +++ b/pkgs/unified_analytics/test/events_with_fake_test.dart @@ -5,8 +5,10 @@ import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:file/memory.dart'; +import 'package:path/path.dart' as p; import 'package:test/test.dart'; +import 'package:unified_analytics/src/constants.dart'; import 'package:unified_analytics/src/enums.dart'; import 'package:unified_analytics/src/survey_handler.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -18,6 +20,7 @@ void main() { late FakeAnalytics fakeAnalytics; late FileSystem fs; late Directory homeDirectory; + late File dismissedSurveyFile; /// Survey to load into the fake instance to fetch /// @@ -49,6 +52,11 @@ void main() { setUp(() async { fs = MemoryFileSystem.test(style: FileSystemStyle.posix); homeDirectory = fs.directory('home'); + dismissedSurveyFile = fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )); final initialAnalytics = Analytics.test( tool: DashTool.flutterTool, @@ -74,6 +82,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [testSurvey], ), ); diff --git a/pkgs/unified_analytics/test/survey_handler_test.dart b/pkgs/unified_analytics/test/survey_handler_test.dart index 908a654c5..11778fb42 100644 --- a/pkgs/unified_analytics/test/survey_handler_test.dart +++ b/pkgs/unified_analytics/test/survey_handler_test.dart @@ -330,6 +330,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -384,6 +385,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -427,6 +429,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -471,7 +474,10 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, fs: fs, content: ''' + homeDirectory: homeDirectory, + fs: fs, + dismissedSurveyFile: dismissedSurveyFile, + content: ''' [ { "uniqueId": "uniqueId123", @@ -573,7 +579,10 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, fs: fs, content: ''' + homeDirectory: homeDirectory, + fs: fs, + dismissedSurveyFile: dismissedSurveyFile, + content: ''' [ { "uniqueId": "uniqueId123", @@ -637,7 +646,10 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, fs: fs, content: ''' + homeDirectory: homeDirectory, + fs: fs, + dismissedSurveyFile: dismissedSurveyFile, + content: ''' [ { "uniqueId": "12345", @@ -722,6 +734,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -799,6 +812,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), ); @@ -845,6 +859,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), ); @@ -893,6 +908,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -923,6 +939,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -945,6 +962,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -992,6 +1010,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1021,6 +1040,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1070,6 +1090,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1103,6 +1124,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1152,6 +1174,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1182,6 +1205,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1207,6 +1231,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -1255,6 +1280,7 @@ void main() { surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', From 4b597f4af9a2930dabc551458802fa6d511480b5 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Thu, 7 Mar 2024 12:43:13 -0500 Subject: [PATCH 05/28] Confighandler no longer late variable --- pkgs/unified_analytics/lib/src/analytics.dart | 145 ++++++++++++------ .../test/events_with_fake_test.dart | 9 ++ 2 files changed, 103 insertions(+), 51 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 38d470483..482345ea0 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -84,6 +84,24 @@ abstract class Analytics { apiSecret: kGoogleAnalyticsApiSecret, ); + final initializer = Initializer( + fs: fs, + tool: tool.label, + homeDirectory: homeDirectory, + toolsMessageVersion: kToolsMessageVersion, + ); + initializer.run(); + final configHandler = ConfigHandler( + fs: fs, + homeDirectory: homeDirectory, + initializer: initializer, + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), + ); + return AnalyticsImpl( tool: tool, homeDirectory: homeDirectory, @@ -106,6 +124,8 @@ abstract class Analytics { enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, + configHandler: configHandler, + initializer: initializer, ); } @@ -162,6 +182,24 @@ abstract class Analytics { apiSecret: kTestApiSecret, ); + final initializer = Initializer( + fs: fs, + tool: tool.label, + homeDirectory: homeDirectory, + toolsMessageVersion: kToolsMessageVersion, + ); + initializer.run(); + final configHandler = ConfigHandler( + fs: fs, + homeDirectory: homeDirectory, + initializer: initializer, + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), + ); + return AnalyticsImpl( tool: tool, homeDirectory: homeDirectory, @@ -184,6 +222,8 @@ abstract class Analytics { enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, + configHandler: configHandler, + initializer: initializer, ); } @@ -206,31 +246,41 @@ abstract class Analytics { GAClient? gaClient, int toolsMessageVersion = kToolsMessageVersion, String toolsMessage = kToolsMessage, - }) => - FakeAnalytics( - tool: tool, - homeDirectory: homeDirectory, - flutterChannel: flutterChannel, - toolsMessageVersion: toolsMessageVersion, - flutterVersion: flutterVersion, - dartVersion: dartVersion, - platform: platform, - fs: fs, - surveyHandler: surveyHandler ?? - FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, - dismissedSurveyFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kDismissedSurveyFileName, - )), - initializedSurveys: [], - ), - gaClient: gaClient ?? const FakeGAClient(), - clientIde: clientIde, - enabledFeatures: enabledFeatures, - ); + }) { + final initializer = Initializer( + fs: fs, + tool: tool.label, + homeDirectory: homeDirectory, + toolsMessageVersion: toolsMessageVersion, + ); + initializer.run(); + + return FakeAnalytics( + tool: tool, + homeDirectory: homeDirectory, + flutterChannel: flutterChannel, + toolsMessageVersion: toolsMessageVersion, + flutterVersion: flutterVersion, + dartVersion: dartVersion, + platform: platform, + fs: fs, + surveyHandler: surveyHandler ?? + FakeSurveyHandler.fromList( + homeDirectory: homeDirectory, + fs: fs, + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), + initializedSurveys: [], + ), + gaClient: gaClient ?? const FakeGAClient(), + clientIde: clientIde, + enabledFeatures: enabledFeatures, + initializer: initializer, + ); + } /// The shared identifier for Flutter and Dart related tooling using /// package:unified_analytics. @@ -341,7 +391,7 @@ abstract class Analytics { class AnalyticsImpl implements Analytics { final DashTool tool; final FileSystem fs; - late final ConfigHandler _configHandler; + final ConfigHandler _configHandler; final GAClient _gaClient; final SurveyHandler _surveyHandler; late String _clientId; @@ -386,9 +436,12 @@ class AnalyticsImpl implements Analytics { required GAClient gaClient, required SurveyHandler surveyHandler, required bool enableAsserts, + required Initializer initializer, + required ConfigHandler configHandler, }) : _gaClient = gaClient, _surveyHandler = surveyHandler, - _enableAsserts = enableAsserts { + _enableAsserts = enableAsserts, + _configHandler = configHandler { // Initialize date formatting for `package:intl` within constructor // so clients using this package won't need to initializeDateFormatting(); @@ -396,13 +449,6 @@ class AnalyticsImpl implements Analytics { // This initializer class will let the instance know // if it was the first run; if it is, nothing will be sent // on the first run - final initializer = Initializer( - fs: fs, - tool: tool.label, - homeDirectory: homeDirectory, - toolsMessageVersion: toolsMessageVersion, - ); - initializer.run(); if (initializer.firstRun) { _showMessage = true; _firstRun = true; @@ -411,18 +457,6 @@ class AnalyticsImpl implements Analytics { _firstRun = false; } - // Create the config handler that will parse the config file - _configHandler = ConfigHandler( - fs: fs, - homeDirectory: homeDirectory, - initializer: initializer, - configFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kConfigFileName, - )), - ); - // Check if the tool has already been onboarded, and if it // has, check if the latest message version is greater to // prompt the client to show a message @@ -764,16 +798,25 @@ class FakeAnalytics extends AnalyticsImpl { required super.platform, required super.fs, required super.surveyHandler, + required super.initializer, super.flutterChannel, super.flutterVersion, super.clientIde, super.enabledFeatures, - int? toolsMessageVersion, - GAClient? gaClient, + super.toolsMessageVersion = kToolsMessageVersion, + super.gaClient = const FakeGAClient(), + super.enableAsserts = true, }) : super( - gaClient: gaClient ?? const FakeGAClient(), - enableAsserts: true, - toolsMessageVersion: toolsMessageVersion ?? kToolsMessageVersion, + configHandler: ConfigHandler( + fs: fs, + homeDirectory: homeDirectory, + initializer: initializer, + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), + ), ); @override diff --git a/pkgs/unified_analytics/test/events_with_fake_test.dart b/pkgs/unified_analytics/test/events_with_fake_test.dart index 5233d955d..8fda247cf 100644 --- a/pkgs/unified_analytics/test/events_with_fake_test.dart +++ b/pkgs/unified_analytics/test/events_with_fake_test.dart @@ -10,6 +10,7 @@ import 'package:test/test.dart'; import 'package:unified_analytics/src/constants.dart'; import 'package:unified_analytics/src/enums.dart'; +import 'package:unified_analytics/src/initializer.dart'; import 'package:unified_analytics/src/survey_handler.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -73,18 +74,26 @@ void main() { // Recreate a second instance since events cannot be sent on // the first run withClock(Clock.fixed(DateTime(2022, 3, 3)), () { + final toolsMessageVersion = kToolsMessageVersion; fakeAnalytics = FakeAnalytics( tool: DashTool.flutterTool, homeDirectory: homeDirectory, dartVersion: 'dartVersion', platform: DevicePlatform.macos, fs: fs, + toolsMessageVersion: toolsMessageVersion, surveyHandler: FakeSurveyHandler.fromList( homeDirectory: homeDirectory, fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [testSurvey], ), + initializer: Initializer( + fs: fs, + tool: DashTool.flutterTool.label, + homeDirectory: homeDirectory, + toolsMessageVersion: toolsMessageVersion, + ), ); }); }); From 5e930210f89103f2f8621e34b2ec37c837eefeb4 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Thu, 7 Mar 2024 13:16:57 -0500 Subject: [PATCH 06/28] Remove `Initializer` dependency from `ConfigHandler` --- pkgs/unified_analytics/lib/src/analytics.dart | 3 --- pkgs/unified_analytics/lib/src/config_handler.dart | 14 +++++++++++--- pkgs/unified_analytics/lib/src/initializer.dart | 14 ++++++-------- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 482345ea0..77e4e4c2e 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -94,7 +94,6 @@ abstract class Analytics { final configHandler = ConfigHandler( fs: fs, homeDirectory: homeDirectory, - initializer: initializer, configFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, @@ -192,7 +191,6 @@ abstract class Analytics { final configHandler = ConfigHandler( fs: fs, homeDirectory: homeDirectory, - initializer: initializer, configFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, @@ -810,7 +808,6 @@ class FakeAnalytics extends AnalyticsImpl { configHandler: ConfigHandler( fs: fs, homeDirectory: homeDirectory, - initializer: initializer, configFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, diff --git a/pkgs/unified_analytics/lib/src/config_handler.dart b/pkgs/unified_analytics/lib/src/config_handler.dart index c525dab47..75cd1e218 100644 --- a/pkgs/unified_analytics/lib/src/config_handler.dart +++ b/pkgs/unified_analytics/lib/src/config_handler.dart @@ -7,7 +7,9 @@ import 'dart:convert'; import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'package:intl/intl.dart'; +import 'package:path/path.dart' as p; +import 'constants.dart'; import 'initializer.dart'; import 'utils.dart'; @@ -33,7 +35,6 @@ class ConfigHandler { final FileSystem fs; final Directory homeDirectory; - final Initializer initializer; final File configFile; final Map parsedTools = {}; @@ -46,7 +47,6 @@ class ConfigHandler { ConfigHandler({ required this.fs, required this.homeDirectory, - required this.initializer, required this.configFile, }) : configFileLastModified = configFile.lastModifiedSync() { // Call the method to parse the contents of the config file when @@ -175,7 +175,15 @@ class ConfigHandler { /// This will reset the configuration file and clear the /// [parsedTools] map and trigger parsing the config again. void resetConfig() { - initializer.run(forceReset: true); + Initializer.createConfigFile( + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), + fs: fs, + homeDirectory: homeDirectory, + ); parsedTools.clear(); parseConfig(); } diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 4f84ef1d5..05bb7b584 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -42,11 +42,10 @@ class Initializer { /// Creates the configuration file with the default message /// in the user's home directory. - void createConfigFile({ + static void createConfigFile({ required File configFile, - required String dateStamp, - required String tool, - required int toolsMessageVersion, + required Directory homeDirectory, + required FileSystem fs, }) { configFile.createSync(recursive: true); @@ -69,7 +68,7 @@ class Initializer { /// Creates that log file that will store the record formatted /// events locally on the user's machine. - void createLogFile({required File logFile}) { + static void createLogFile({required File logFile}) { logFile.createSync(recursive: true); } @@ -106,9 +105,8 @@ class Initializer { firstRun = true; createConfigFile( configFile: configFile, - dateStamp: dateStamp, - tool: tool, - toolsMessageVersion: toolsMessageVersion, + fs: fs, + homeDirectory: homeDirectory, ); } From 1640a33cb97afd5caa0fccdf5982457a2c6de1de Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Thu, 7 Mar 2024 15:49:38 -0500 Subject: [PATCH 07/28] Remove `late` variable for `_clientId` --- pkgs/unified_analytics/lib/src/analytics.dart | 40 ++++++++++++------- .../lib/src/initializer.dart | 8 ++-- 2 files changed, 29 insertions(+), 19 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 77e4e4c2e..153ea1aff 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -392,8 +392,7 @@ class AnalyticsImpl implements Analytics { final ConfigHandler _configHandler; final GAClient _gaClient; final SurveyHandler _surveyHandler; - late String _clientId; - late final File _clientIdFile; + final File _clientIdFile; late final UserProperty userProperty; late final LogHandler _logHandler; late final Session _sessionHandler; @@ -420,6 +419,9 @@ class AnalyticsImpl implements Analytics { /// from the [GAClient]. final _futures = >[]; + /// Internal value for the client id which will be lazily loaded. + String? _clientId; + AnalyticsImpl({ required this.tool, required Directory homeDirectory, @@ -439,7 +441,12 @@ class AnalyticsImpl implements Analytics { }) : _gaClient = gaClient, _surveyHandler = surveyHandler, _enableAsserts = enableAsserts, - _configHandler = configHandler { + _configHandler = configHandler, + _clientIdFile = fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kClientIdFileName, + )) { // Initialize date formatting for `package:intl` within constructor // so clients using this package won't need to initializeDateFormatting(); @@ -472,10 +479,6 @@ class AnalyticsImpl implements Analytics { _firstRun = true; } - _clientIdFile = fs.file( - p.join(homeDirectory.path, kDartToolDirectoryName, kClientIdFileName)); - _clientId = _clientIdFile.readAsStringSync(); - // Initialization for the error handling class that will prevent duplicate // [Event.analyticsException] events from being sent to GA4 _errorHandler = ErrorHandler(sendFunction: send); @@ -528,7 +531,14 @@ class AnalyticsImpl implements Analytics { } @override - String get clientId => _clientId; + String get clientId { + if (!_clientIdFile.existsSync()) { + Initializer.createClientIdFile(clientIdFile: _clientIdFile); + } + _clientId ??= _clientIdFile.readAsStringSync(); + + return _clientId!; + } @override String get getConsentMessage { @@ -623,7 +633,7 @@ class AnalyticsImpl implements Analytics { // Apply the survey's sample rate; if the generated value from // the client id and survey's uniqueId are less, it will not get // sent to the user - if (survey.samplingRate < sampleRate(_clientId, survey.uniqueId)) { + if (survey.samplingRate < sampleRate(clientId, survey.uniqueId)) { continue; } @@ -678,7 +688,7 @@ class AnalyticsImpl implements Analytics { // Construct the body of the request final body = generateRequestBody( - clientId: _clientId, + clientId: clientId, eventName: event.eventName, eventData: event.eventData, userProperty: userProperty, @@ -708,7 +718,7 @@ class AnalyticsImpl implements Analytics { // Recreate the session and client id file; no need to // recreate the log file since it will only receives events // to persist from events sent - Initializer.createClientIdFile(clientFile: _clientIdFile); + Initializer.createClientIdFile(clientIdFile: _clientIdFile); Initializer.createSessionFile(sessionFile: _sessionHandler.sessionFile); // Reread the client ID string so an empty string is not being @@ -719,7 +729,7 @@ class AnalyticsImpl implements Analytics { // We must construct the body at this point after we have read in the // new client id string that was generated body = generateRequestBody( - clientId: _clientId, + clientId: clientId, eventName: collectionEvent.eventName, eventData: collectionEvent.eventData, userProperty: userProperty, @@ -730,7 +740,7 @@ class AnalyticsImpl implements Analytics { // Construct the body of the request to signal // telemetry status toggling body = generateRequestBody( - clientId: _clientId, + clientId: clientId, eventName: collectionEvent.eventName, eventData: collectionEvent.eventData, userProperty: userProperty, @@ -741,7 +751,7 @@ class AnalyticsImpl implements Analytics { _logHandler.logFile.writeAsStringSync(''); _clientIdFile.writeAsStringSync(''); - _clientId = _clientIdFile.readAsStringSync(); + _clientId = ''; } // Pass to the google analytics client to send with a @@ -822,7 +832,7 @@ class FakeAnalytics extends AnalyticsImpl { // Construct the body of the request final body = generateRequestBody( - clientId: _clientId, + clientId: clientId, eventName: event.eventName, eventData: event.eventData, userProperty: userProperty, diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 05bb7b584..f6b7c3bdb 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -35,9 +35,9 @@ class Initializer { /// Creates the text file that will contain the client ID /// which will be used across all related tools for analytics /// reporting in GA. - static void createClientIdFile({required File clientFile}) { - clientFile.createSync(recursive: true); - clientFile.writeAsStringSync(Uuid().generateV4()); + static void createClientIdFile({required File clientIdFile}) { + clientIdFile.createSync(recursive: true); + clientIdFile.writeAsStringSync(Uuid().generateV4()); } /// Creates the configuration file with the default message @@ -114,7 +114,7 @@ class Initializer { final clientFile = fs.file( p.join(homeDirectory.path, kDartToolDirectoryName, kClientIdFileName)); if (!clientFile.existsSync()) { - createClientIdFile(clientFile: clientFile); + createClientIdFile(clientIdFile: clientFile); } // Begin initialization checks for the session file From dd9ffe451d244b11866dfb7beda275d3f5f7d08e Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:29:27 -0500 Subject: [PATCH 08/28] Consolidate `Session` into `UserProperty` --- pkgs/unified_analytics/lib/src/analytics.dart | 20 +-- pkgs/unified_analytics/lib/src/session.dart | 123 ------------------ .../lib/src/user_property.dart | 120 +++++++++++++++-- .../test/unified_analytics_test.dart | 6 + 4 files changed, 123 insertions(+), 146 deletions(-) delete mode 100644 pkgs/unified_analytics/lib/src/session.dart diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 153ea1aff..b943eacca 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -21,7 +21,6 @@ import 'event.dart'; import 'ga_client.dart'; import 'initializer.dart'; import 'log_handler.dart'; -import 'session.dart'; import 'survey_handler.dart'; import 'user_property.dart'; import 'utils.dart'; @@ -395,7 +394,6 @@ class AnalyticsImpl implements Analytics { final File _clientIdFile; late final UserProperty userProperty; late final LogHandler _logHandler; - late final Session _sessionHandler; late final ErrorHandler _errorHandler; final int toolsMessageVersion; @@ -487,18 +485,12 @@ class AnalyticsImpl implements Analytics { // each event that is sent to Google Analytics -- it will be responsible // for getting the session id or rolling the session if the duration // exceeds [kSessionDurationMinutes] - _sessionHandler = Session( - homeDirectory: homeDirectory, - fs: fs, - errorHandler: _errorHandler, + userProperty = UserProperty( sessionFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, kSessionFileName, )), - ); - userProperty = UserProperty( - session: _sessionHandler, flutterChannel: flutterChannel, host: platform.label, flutterVersion: flutterVersion, @@ -511,6 +503,8 @@ class AnalyticsImpl implements Analytics { locale: io.Platform.localeName, clientIde: clientIde, enabledFeatures: enabledFeatures, + errorHandler: _errorHandler, + telemetryEnabled: telemetryEnabled, ); // Initialize the log handler to persist events that are being sent @@ -524,10 +518,6 @@ class AnalyticsImpl implements Analytics { kLogFileName, )), ); - - // Initialize the session handler with the session_id - // by parsing the json file - _sessionHandler.initialize(telemetryEnabled); } @override @@ -719,7 +709,7 @@ class AnalyticsImpl implements Analytics { // recreate the log file since it will only receives events // to persist from events sent Initializer.createClientIdFile(clientIdFile: _clientIdFile); - Initializer.createSessionFile(sessionFile: _sessionHandler.sessionFile); + Initializer.createSessionFile(sessionFile: userProperty.sessionFile); // Reread the client ID string so an empty string is not being // sent to GA4 since the persisted files are cleared when a user @@ -747,7 +737,7 @@ class AnalyticsImpl implements Analytics { ); // For opted out users, data in the persisted files is cleared - _sessionHandler.sessionFile.writeAsStringSync(''); + userProperty.sessionFile.writeAsStringSync(''); _logHandler.logFile.writeAsStringSync(''); _clientIdFile.writeAsStringSync(''); diff --git a/pkgs/unified_analytics/lib/src/session.dart b/pkgs/unified_analytics/lib/src/session.dart deleted file mode 100644 index 503b43e99..000000000 --- a/pkgs/unified_analytics/lib/src/session.dart +++ /dev/null @@ -1,123 +0,0 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'dart:convert'; - -import 'package:clock/clock.dart'; -import 'package:file/file.dart'; - -import 'constants.dart'; -import 'error_handler.dart'; -import 'event.dart'; -import 'initializer.dart'; - -class Session { - final Directory homeDirectory; - final FileSystem fs; - final File sessionFile; - final ErrorHandler _errorHandler; - - int? _sessionId; - - Session({ - required this.homeDirectory, - required this.fs, - required ErrorHandler errorHandler, - required this.sessionFile, - }) : _errorHandler = errorHandler; - - /// This will use the data parsed from the - /// session file in the dart-tool directory - /// to get the session id if the last ping was within - /// [kSessionDurationMinutes]. - /// - /// If time since last ping exceeds the duration, then the file - /// will be updated with a new session id and that will be returned. - /// - /// Note, the file will always be updated when calling this method - /// because the last ping variable will always need to be persisted. - int? getSessionId() { - _refreshSessionData(); - final now = clock.now(); - - // Convert the epoch time from the last ping into datetime and check if we - // are within the kSessionDurationMinutes. - final lastPingDateTime = sessionFile.lastModifiedSync(); - if (now.difference(lastPingDateTime).inMinutes > kSessionDurationMinutes) { - // Update the session file with the latest session id - _sessionId = now.millisecondsSinceEpoch; - sessionFile.writeAsStringSync('{"session_id": $_sessionId}'); - } else { - // Update the last modified timestamp with the current timestamp so that - // we can use it for the next _lastPing calculation - sessionFile.setLastModifiedSync(now); - } - - return _sessionId; - } - - /// Preps the [Session] class with the data found in the session file. - /// - /// We must check if telemetry is enabled to refresh the session data - /// because the refresh method will write to the session file and for - /// users that have opted out, we have to leave the session file empty - /// per the privacy document - void initialize(bool telemetryEnabled) { - if (telemetryEnabled) _refreshSessionData(); - } - - /// This will go to the session file within the dart-tool - /// directory and fetch the latest data from the session file to update - /// the class's variables. If the session file is malformed, a new - /// session file will be recreated. - /// - /// This allows the session data in this class to always be up - /// to date incase another tool is also calling this package and - /// making updates to the session file. - void _refreshSessionData() { - /// Using a nested function here to reduce verbosity - void parseContents() { - final sessionFileContents = sessionFile.readAsStringSync(); - final sessionObj = - jsonDecode(sessionFileContents) as Map; - _sessionId = sessionObj['session_id'] as int; - } - - try { - // Failing to parse the contents will result in the current timestamp - // being used as the session id and will get used to recreate the file - parseContents(); - } on FormatException catch (err) { - final now = clock.now(); - Initializer.createSessionFile( - sessionFile: sessionFile, - sessionIdOverride: now, - ); - - _errorHandler.log(Event.analyticsException( - workflow: 'Session._refreshSessionData', - error: err.runtimeType.toString(), - description: 'message: ${err.message}\nsource: ${err.source}', - )); - - // Fallback to setting the session id as the current time - _sessionId = now.millisecondsSinceEpoch; - } on FileSystemException catch (err) { - final now = clock.now(); - Initializer.createSessionFile( - sessionFile: sessionFile, - sessionIdOverride: now, - ); - - _errorHandler.log(Event.analyticsException( - workflow: 'Session._refreshSessionData', - error: err.runtimeType.toString(), - description: err.osError?.toString(), - )); - - // Fallback to setting the session id as the current time - _sessionId = now.millisecondsSinceEpoch; - } - } -} diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index 33d0e475f..d4133d088 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -5,13 +5,15 @@ import 'dart:convert'; import 'package:clock/clock.dart'; +import 'package:file/file.dart'; import 'constants.dart'; -import 'session.dart'; +import 'error_handler.dart'; +import 'event.dart'; +import 'initializer.dart'; import 'utils.dart'; class UserProperty { - final Session session; final String? flutterChannel; final String host; final String? flutterVersion; @@ -22,11 +24,15 @@ class UserProperty { final String? clientIde; final String? enabledFeatures; + final File sessionFile; + final ErrorHandler _errorHandler; + + int? _sessionId; + /// This class is intended to capture all of the user's /// metadata when the class gets initialized as well as collecting /// session data to send in the json payload to Google Analytics. UserProperty({ - required this.session, required this.flutterChannel, required this.host, required this.flutterVersion, @@ -36,13 +42,17 @@ class UserProperty { required this.locale, required this.clientIde, required this.enabledFeatures, - }); + required this.sessionFile, + required ErrorHandler errorHandler, + required bool telemetryEnabled, + }) : _errorHandler = errorHandler; /// This method will take the data in this class and convert it into /// a Map that is suitable for the POST request schema. /// - /// This will call the [Session] object's [Session.getSessionId] method which - /// will update the session file and get a new session id if necessary. + /// This will call the [UserProperty] object's [UserProperty.getSessionId] + /// method which will update the session file and get a new session id + /// if necessary. /// /// https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?client_type=gtag Map> preparePayload() { @@ -58,9 +68,9 @@ class UserProperty { } /// Convert the data stored in this class into a map while also - /// getting the latest session id using the [Session] class. + /// getting the latest session id using the [UserProperty] class. Map _toMap() => { - 'session_id': session.getSessionId(), + 'session_id': getSessionId(), 'flutter_channel': flutterChannel, 'host': host, 'flutter_version': flutterVersion, @@ -73,4 +83,98 @@ class UserProperty { 'client_ide': clientIde, 'enabled_features': enabledFeatures, }; + + /// This will use the data parsed from the + /// session file in the dart-tool directory + /// to get the session id if the last ping was within + /// [kSessionDurationMinutes]. + /// + /// If time since last ping exceeds the duration, then the file + /// will be updated with a new session id and that will be returned. + /// + /// Note, the file will always be updated when calling this method + /// because the last ping variable will always need to be persisted. + int? getSessionId() { + _refreshSessionData(); + final now = clock.now(); + + // Convert the epoch time from the last ping into datetime and check if we + // are within the kSessionDurationMinutes. + final lastPingDateTime = sessionFile.lastModifiedSync(); + if (now.difference(lastPingDateTime).inMinutes > kSessionDurationMinutes) { + // Update the session file with the latest session id + _sessionId = now.millisecondsSinceEpoch; + sessionFile.writeAsStringSync('{"session_id": $_sessionId}'); + } else { + // Update the last modified timestamp with the current timestamp so that + // we can use it for the next _lastPing calculation + sessionFile.setLastModifiedSync(now); + } + + return _sessionId; + } + + /// Preps the [UserProperty] class with the data found in the session file. + /// + /// We must check if telemetry is enabled to refresh the session data + /// because the refresh method will write to the session file and for + /// users that have opted out, we have to leave the session file empty + /// per the privacy document + void initialize(bool telemetryEnabled) { + if (telemetryEnabled) _refreshSessionData(); + } + + /// This will go to the session file within the dart-tool + /// directory and fetch the latest data from the session file to update + /// the class's variables. If the session file is malformed, a new + /// session file will be recreated. + /// + /// This allows the session data in this class to always be up + /// to date incase another tool is also calling this package and + /// making updates to the session file. + void _refreshSessionData() { + /// Using a nested function here to reduce verbosity + void parseContents() { + final sessionFileContents = sessionFile.readAsStringSync(); + final sessionObj = + jsonDecode(sessionFileContents) as Map; + _sessionId = sessionObj['session_id'] as int; + } + + try { + // Failing to parse the contents will result in the current timestamp + // being used as the session id and will get used to recreate the file + parseContents(); + } on FormatException catch (err) { + final now = clock.now(); + Initializer.createSessionFile( + sessionFile: sessionFile, + sessionIdOverride: now, + ); + + _errorHandler.log(Event.analyticsException( + workflow: 'Session._refreshSessionData', + error: err.runtimeType.toString(), + description: 'message: ${err.message}\nsource: ${err.source}', + )); + + // Fallback to setting the session id as the current time + _sessionId = now.millisecondsSinceEpoch; + } on FileSystemException catch (err) { + final now = clock.now(); + Initializer.createSessionFile( + sessionFile: sessionFile, + sessionIdOverride: now, + ); + + _errorHandler.log(Event.analyticsException( + workflow: 'Session._refreshSessionData', + error: err.runtimeType.toString(), + description: err.osError?.toString(), + )); + + // Fallback to setting the session id as the current time + _sessionId = now.millisecondsSinceEpoch; + } + } } diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 8c0849338..6fc0261db 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -177,6 +177,12 @@ void main() { ) as FakeAnalytics; analytics.clientShowedMessage(); + // Invoking a send command should reset the session file to a good state + // + // Having it reformat the session file before any send event happens will just + // add additional work on startup + analytics.send(testEvent); + final errorEvent = analytics.sentEvents .where((element) => element.eventName == DashEvent.analyticsException) .firstOrNull; From 0052d718b39e42295c603db2fd56220de307d817 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:30:22 -0500 Subject: [PATCH 09/28] Sort members + swap error messages for tests --- .../lib/src/user_property.dart | 78 +++++++++---------- .../test/unified_analytics_test.dart | 6 +- 2 files changed, 42 insertions(+), 42 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index d4133d088..890bccafb 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -47,43 +47,6 @@ class UserProperty { required bool telemetryEnabled, }) : _errorHandler = errorHandler; - /// This method will take the data in this class and convert it into - /// a Map that is suitable for the POST request schema. - /// - /// This will call the [UserProperty] object's [UserProperty.getSessionId] - /// method which will update the session file and get a new session id - /// if necessary. - /// - /// https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?client_type=gtag - Map> preparePayload() { - return >{ - for (MapEntry entry in _toMap().entries) - entry.key: {'value': entry.value} - }; - } - - @override - String toString() { - return jsonEncode(_toMap()); - } - - /// Convert the data stored in this class into a map while also - /// getting the latest session id using the [UserProperty] class. - Map _toMap() => { - 'session_id': getSessionId(), - 'flutter_channel': flutterChannel, - 'host': host, - 'flutter_version': flutterVersion, - 'dart_version': dartVersion, - 'analytics_pkg_version': kPackageVersion, - 'tool': tool, - 'local_time': formatDateTime(clock.now()), - 'host_os_version': hostOsVersion, - 'locale': locale, - 'client_ide': clientIde, - 'enabled_features': enabledFeatures, - }; - /// This will use the data parsed from the /// session file in the dart-tool directory /// to get the session id if the last ping was within @@ -124,6 +87,26 @@ class UserProperty { if (telemetryEnabled) _refreshSessionData(); } + /// This method will take the data in this class and convert it into + /// a Map that is suitable for the POST request schema. + /// + /// This will call the [UserProperty] object's [UserProperty.getSessionId] + /// method which will update the session file and get a new session id + /// if necessary. + /// + /// https://developers.google.com/analytics/devguides/collection/protocol/ga4/user-properties?client_type=gtag + Map> preparePayload() { + return >{ + for (MapEntry entry in _toMap().entries) + entry.key: {'value': entry.value} + }; + } + + @override + String toString() { + return jsonEncode(_toMap()); + } + /// This will go to the session file within the dart-tool /// directory and fetch the latest data from the session file to update /// the class's variables. If the session file is malformed, a new @@ -153,7 +136,7 @@ class UserProperty { ); _errorHandler.log(Event.analyticsException( - workflow: 'Session._refreshSessionData', + workflow: 'UserProperty._refreshSessionData', error: err.runtimeType.toString(), description: 'message: ${err.message}\nsource: ${err.source}', )); @@ -168,7 +151,7 @@ class UserProperty { ); _errorHandler.log(Event.analyticsException( - workflow: 'Session._refreshSessionData', + workflow: 'UserProperty._refreshSessionData', error: err.runtimeType.toString(), description: err.osError?.toString(), )); @@ -177,4 +160,21 @@ class UserProperty { _sessionId = now.millisecondsSinceEpoch; } } + + /// Convert the data stored in this class into a map while also + /// getting the latest session id using the [UserProperty] class. + Map _toMap() => { + 'session_id': getSessionId(), + 'flutter_channel': flutterChannel, + 'host': host, + 'flutter_version': flutterVersion, + 'dart_version': dartVersion, + 'analytics_pkg_version': kPackageVersion, + 'tool': tool, + 'local_time': formatDateTime(clock.now()), + 'host_os_version': hostOsVersion, + 'locale': locale, + 'client_ide': clientIde, + 'enabled_features': enabledFeatures, + }; } diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 6fc0261db..187216f43 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -150,7 +150,7 @@ void main() { final lastEvent = analytics.sentEvents.last; expect(lastEvent, isNotNull); expect(lastEvent.eventName, DashEvent.analyticsException); - expect(lastEvent.eventData['workflow']!, 'Session._refreshSessionData'); + expect(lastEvent.eventData['workflow']!, 'UserProperty._refreshSessionData'); expect(lastEvent.eventData['error']!, 'FormatException'); }); }); @@ -187,7 +187,7 @@ void main() { .where((element) => element.eventName == DashEvent.analyticsException) .firstOrNull; expect(errorEvent, isNotNull); - expect(errorEvent!.eventData['workflow'], 'Session._refreshSessionData'); + expect(errorEvent!.eventData['workflow'], 'UserProperty._refreshSessionData'); expect(errorEvent.eventData['error'], 'FormatException'); expect(errorEvent.eventData['description'], 'message: Unexpected character\nsource: not a valid session id'); @@ -212,7 +212,7 @@ void main() { final lastEvent = analytics.sentEvents.last; expect(lastEvent, isNotNull); expect(lastEvent.eventName, DashEvent.analyticsException); - expect(lastEvent.eventData['workflow']!, 'Session._refreshSessionData'); + expect(lastEvent.eventData['workflow']!, 'UserProperty._refreshSessionData'); expect(lastEvent.eventData['error']!, 'FileSystemException'); }); }); From 9394d29ddb1efa8597f548b106c4d3f20b4c4928 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 8 Mar 2024 08:31:24 -0500 Subject: [PATCH 10/28] `dart format .` --- pkgs/unified_analytics/test/unified_analytics_test.dart | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 187216f43..c551a92b1 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -150,7 +150,8 @@ void main() { final lastEvent = analytics.sentEvents.last; expect(lastEvent, isNotNull); expect(lastEvent.eventName, DashEvent.analyticsException); - expect(lastEvent.eventData['workflow']!, 'UserProperty._refreshSessionData'); + expect( + lastEvent.eventData['workflow']!, 'UserProperty._refreshSessionData'); expect(lastEvent.eventData['error']!, 'FormatException'); }); }); @@ -187,7 +188,8 @@ void main() { .where((element) => element.eventName == DashEvent.analyticsException) .firstOrNull; expect(errorEvent, isNotNull); - expect(errorEvent!.eventData['workflow'], 'UserProperty._refreshSessionData'); + expect( + errorEvent!.eventData['workflow'], 'UserProperty._refreshSessionData'); expect(errorEvent.eventData['error'], 'FormatException'); expect(errorEvent.eventData['description'], 'message: Unexpected character\nsource: not a valid session id'); @@ -212,7 +214,8 @@ void main() { final lastEvent = analytics.sentEvents.last; expect(lastEvent, isNotNull); expect(lastEvent.eventName, DashEvent.analyticsException); - expect(lastEvent.eventData['workflow']!, 'UserProperty._refreshSessionData'); + expect( + lastEvent.eventData['workflow']!, 'UserProperty._refreshSessionData'); expect(lastEvent.eventData['error']!, 'FileSystemException'); }); }); From f0d110e0f69082f073a1f05416eb97ab2bf84bc3 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 8 Mar 2024 10:36:47 -0500 Subject: [PATCH 11/28] Fix tests + note a TODO for `package:file` --- pkgs/unified_analytics/lib/src/initializer.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index f6b7c3bdb..74bf55a37 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -85,6 +85,8 @@ class Initializer { sessionFile.createSync(recursive: true); sessionFile .writeAsStringSync('{"session_id": ${now.millisecondsSinceEpoch}}'); + // TODO: eliasyishak, remove the below once https://github.com/google/file.dart/issues/236 is fixed + sessionFile.setLastModifiedSync(clock.now()); } /// This will check that there is a client ID populated in From 173d5d85104bcc77d20b3069ff23cd4a71f69995 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Sat, 9 Mar 2024 09:18:55 -0500 Subject: [PATCH 12/28] `..run()` --- pkgs/unified_analytics/lib/src/analytics.dart | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index b943eacca..bf1e14d34 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -88,8 +88,7 @@ abstract class Analytics { tool: tool.label, homeDirectory: homeDirectory, toolsMessageVersion: kToolsMessageVersion, - ); - initializer.run(); + )..run(); final configHandler = ConfigHandler( fs: fs, homeDirectory: homeDirectory, @@ -185,8 +184,7 @@ abstract class Analytics { tool: tool.label, homeDirectory: homeDirectory, toolsMessageVersion: kToolsMessageVersion, - ); - initializer.run(); + )..run(); final configHandler = ConfigHandler( fs: fs, homeDirectory: homeDirectory, @@ -249,8 +247,7 @@ abstract class Analytics { tool: tool.label, homeDirectory: homeDirectory, toolsMessageVersion: toolsMessageVersion, - ); - initializer.run(); + )..run(); return FakeAnalytics( tool: tool, From d7423155bc635a630809b53a7ce53246f658ad69 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 11:06:56 -0400 Subject: [PATCH 13/28] Remove `UserProperty.initialize` method --- pkgs/unified_analytics/lib/src/user_property.dart | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index 890bccafb..6c37d9628 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -77,16 +77,6 @@ class UserProperty { return _sessionId; } - /// Preps the [UserProperty] class with the data found in the session file. - /// - /// We must check if telemetry is enabled to refresh the session data - /// because the refresh method will write to the session file and for - /// users that have opted out, we have to leave the session file empty - /// per the privacy document - void initialize(bool telemetryEnabled) { - if (telemetryEnabled) _refreshSessionData(); - } - /// This method will take the data in this class and convert it into /// a Map that is suitable for the POST request schema. /// From 1aea01034fd97ea4dbbac640c629b4e3be2f9185 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:26:55 -0400 Subject: [PATCH 14/28] Remove `ErrorHandler` + no late for userprop and loghandler --- pkgs/unified_analytics/lib/src/analytics.dart | 116 +++++++++++------- .../lib/src/error_handler.dart | 32 ----- .../lib/src/log_handler.dart | 11 +- .../lib/src/user_property.dart | 11 +- .../test/error_handler_test.dart | 11 ++ .../test/log_handler_test.dart | 8 +- .../test/unified_analytics_test.dart | 6 + 7 files changed, 102 insertions(+), 93 deletions(-) delete mode 100644 pkgs/unified_analytics/lib/src/error_handler.dart diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index bf1e14d34..a2d85ef7b 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -16,7 +16,6 @@ import 'asserts.dart'; import 'config_handler.dart'; import 'constants.dart'; import 'enums.dart'; -import 'error_handler.dart'; import 'event.dart'; import 'ga_client.dart'; import 'initializer.dart'; @@ -389,9 +388,8 @@ class AnalyticsImpl implements Analytics { final GAClient _gaClient; final SurveyHandler _surveyHandler; final File _clientIdFile; - late final UserProperty userProperty; - late final LogHandler _logHandler; - late final ErrorHandler _errorHandler; + final UserProperty userProperty; + final LogHandler _logHandler; final int toolsMessageVersion; /// Tells the client if they need to show a message to the @@ -417,6 +415,13 @@ class AnalyticsImpl implements Analytics { /// Internal value for the client id which will be lazily loaded. String? _clientId; + /// Internal collection of [Event]s that have been sent + /// for errors encountered within package:unified_analytics. + /// + /// Stores each of the events that have been sent to GA4 so that the + /// same error doesn't get sent twice. + final Set _sentErrorEvents = {}; + AnalyticsImpl({ required this.tool, required Directory homeDirectory, @@ -441,7 +446,35 @@ class AnalyticsImpl implements Analytics { homeDirectory.path, kDartToolDirectoryName, kClientIdFileName, - )) { + )), + userProperty = UserProperty( + sessionFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kSessionFileName, + )), + flutterChannel: flutterChannel, + host: platform.label, + flutterVersion: flutterVersion, + dartVersion: dartVersion, + tool: tool.label, + // We truncate this to a maximum of 36 characters since this can + // a very long string for some operating systems + hostOsVersion: + truncateStringToLength(io.Platform.operatingSystemVersion, 36), + locale: io.Platform.localeName, + clientIde: clientIde, + enabledFeatures: enabledFeatures, + ), + _logHandler = LogHandler( + fs: fs, + homeDirectory: homeDirectory, + logFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kLogFileName, + )), + ) { // Initialize date formatting for `package:intl` within constructor // so clients using this package won't need to initializeDateFormatting(); @@ -473,48 +506,6 @@ class AnalyticsImpl implements Analytics { // will be blocked _firstRun = true; } - - // Initialization for the error handling class that will prevent duplicate - // [Event.analyticsException] events from being sent to GA4 - _errorHandler = ErrorHandler(sendFunction: send); - - // Initialize the user property class that will be attached to - // each event that is sent to Google Analytics -- it will be responsible - // for getting the session id or rolling the session if the duration - // exceeds [kSessionDurationMinutes] - userProperty = UserProperty( - sessionFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kSessionFileName, - )), - flutterChannel: flutterChannel, - host: platform.label, - flutterVersion: flutterVersion, - dartVersion: dartVersion, - tool: tool.label, - // We truncate this to a maximum of 36 characters since this can - // a very long string for some operating systems - hostOsVersion: - truncateStringToLength(io.Platform.operatingSystemVersion, 36), - locale: io.Platform.localeName, - clientIde: clientIde, - enabledFeatures: enabledFeatures, - errorHandler: _errorHandler, - telemetryEnabled: telemetryEnabled, - ); - - // Initialize the log handler to persist events that are being sent - _logHandler = LogHandler( - fs: fs, - homeDirectory: homeDirectory, - errorHandler: _errorHandler, - logFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kLogFileName, - )), - ); } @override @@ -594,6 +585,9 @@ class AnalyticsImpl implements Analytics { @override Future close({int delayDuration = kDelayDuration}) async { + // Collect any errors encountered and send + _sendPendingErrorEvents(); + await Future.wait(_futures).timeout( Duration(milliseconds: delayDuration), onTimeout: () => [], @@ -771,6 +765,30 @@ class AnalyticsImpl implements Analytics { _surveyHandler.dismiss(survey, false); send(Event.surveyShown(surveyId: survey.uniqueId)); } + + /// Send any pending error events, useful for tests to avoid closing + /// the connection. + /// + /// In the main implementation, [AnalyticsImpl], error events are only + /// sent on exit when [close] is invoked. This helper method can instead + /// have those error events sent immediately to help with tests that check + /// [FakeAnalytics.sentEvents]. + void _sendPendingErrorEvents() { + // Collect any errors encountered and send + final errorEvents = {...userProperty.errorSet, ..._logHandler.errorSet}; + errorEvents + .where((event) => + event.eventName == DashEvent.analyticsException && + !_sentErrorEvents.contains(event)) + .forEach(send); + + // Ensure the same event doesn't get sent again + _sentErrorEvents.addAll(errorEvents); + + // Clear error sets + userProperty.errorSet.clear(); + _logHandler.errorSet.clear(); + } } /// This fake instance of [Analytics] is intended to be used by clients of @@ -833,6 +851,10 @@ class FakeAnalytics extends AnalyticsImpl { // for internal methods in the `Analytics` instance sentEvents.add(event); } + + /// Public instance method to invoke private method that sends any + /// pending error events. + void sendPendingErrorEvents() => _sendPendingErrorEvents(); } /// An implementation that will never send events. diff --git a/pkgs/unified_analytics/lib/src/error_handler.dart b/pkgs/unified_analytics/lib/src/error_handler.dart deleted file mode 100644 index 5937db93a..000000000 --- a/pkgs/unified_analytics/lib/src/error_handler.dart +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file -// for details. All rights reserved. Use of this source code is governed by a -// BSD-style license that can be found in the LICENSE file. - -import 'analytics.dart'; -import 'enums.dart'; -import 'event.dart'; - -class ErrorHandler { - /// Stores each of the events that have been sent to GA4 so that the - /// same error doesn't get sent twice. - final Set _sentErrorEvents = {}; - final SendFunction _sendFunction; - - /// Handles any errors encountered within package:unified_analytics. - ErrorHandler({required SendFunction sendFunction}) - : _sendFunction = sendFunction; - - /// Sends the encountered error [Event.analyticsException] to GA4 backend. - /// - /// This method will not send the event to GA4 if it has already been - /// sent before during the current process. - void log(Event event) { - if (event.eventName != DashEvent.analyticsException || - _sentErrorEvents.contains(event)) { - return; - } - - _sendFunction(event); - _sentErrorEvents.add(event); - } -} diff --git a/pkgs/unified_analytics/lib/src/log_handler.dart b/pkgs/unified_analytics/lib/src/log_handler.dart index c4f950c46..eafe36689 100644 --- a/pkgs/unified_analytics/lib/src/log_handler.dart +++ b/pkgs/unified_analytics/lib/src/log_handler.dart @@ -8,7 +8,6 @@ import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'constants.dart'; -import 'error_handler.dart'; import 'event.dart'; import 'initializer.dart'; @@ -161,16 +160,16 @@ class LogHandler { final FileSystem fs; final Directory homeDirectory; final File logFile; - final ErrorHandler _errorHandler; + + final Set errorSet = {}; /// A log handler constructor that will delegate saving /// logs and retrieving stats from the persisted log. LogHandler({ required this.fs, required this.homeDirectory, - required ErrorHandler errorHandler, required this.logFile, - }) : _errorHandler = errorHandler; + }); /// Get stats from the persisted log file. /// @@ -187,7 +186,7 @@ class LogHandler { try { return LogItem.fromRecord(jsonDecode(e) as Map); } on FormatException catch (err) { - _errorHandler.log(Event.analyticsException( + errorSet.add(Event.analyticsException( workflow: 'LogFileStats.logFileStats', error: err.runtimeType.toString(), description: 'message: ${err.message}\nsource: ${err.source}', @@ -196,7 +195,7 @@ class LogHandler { return null; // ignore: avoid_catching_errors } on TypeError catch (err) { - _errorHandler.log(Event.analyticsException( + errorSet.add(Event.analyticsException( workflow: 'LogFileStats.logFileStats', error: err.runtimeType.toString(), )); diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index 6c37d9628..e12117c62 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -8,7 +8,6 @@ import 'package:clock/clock.dart'; import 'package:file/file.dart'; import 'constants.dart'; -import 'error_handler.dart'; import 'event.dart'; import 'initializer.dart'; import 'utils.dart'; @@ -25,7 +24,7 @@ class UserProperty { final String? enabledFeatures; final File sessionFile; - final ErrorHandler _errorHandler; + final Set errorSet = {}; int? _sessionId; @@ -43,9 +42,7 @@ class UserProperty { required this.clientIde, required this.enabledFeatures, required this.sessionFile, - required ErrorHandler errorHandler, - required bool telemetryEnabled, - }) : _errorHandler = errorHandler; + }); /// This will use the data parsed from the /// session file in the dart-tool directory @@ -125,7 +122,7 @@ class UserProperty { sessionIdOverride: now, ); - _errorHandler.log(Event.analyticsException( + errorSet.add(Event.analyticsException( workflow: 'UserProperty._refreshSessionData', error: err.runtimeType.toString(), description: 'message: ${err.message}\nsource: ${err.source}', @@ -140,7 +137,7 @@ class UserProperty { sessionIdOverride: now, ); - _errorHandler.log(Event.analyticsException( + errorSet.add(Event.analyticsException( workflow: 'UserProperty._refreshSessionData', error: err.runtimeType.toString(), description: err.osError?.toString(), diff --git a/pkgs/unified_analytics/test/error_handler_test.dart b/pkgs/unified_analytics/test/error_handler_test.dart index c7156e2aa..22decf72d 100644 --- a/pkgs/unified_analytics/test/error_handler_test.dart +++ b/pkgs/unified_analytics/test/error_handler_test.dart @@ -139,6 +139,7 @@ void main() { analytics.send(testEvent); expect(sessionFile.readAsStringSync(), isNotEmpty); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -167,6 +168,7 @@ void main() { expect(sessionFile.existsSync(), isFalse); analytics.send(testEvent); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -198,6 +200,7 @@ void main() { analytics.send(testEvent); expect(sessionFile.readAsStringSync(), isNotEmpty); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -210,6 +213,7 @@ void main() { analytics.send(testEvent); expect(sessionFile.readAsStringSync(), isNotEmpty); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -243,6 +247,7 @@ void main() { analytics.send(testEvent); expect(analytics.sentEvents, hasLength(1)); expect(logFile.readAsLinesSync(), hasLength(3)); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -254,6 +259,7 @@ void main() { expect(logFileStats, isNotNull); expect(logFileStats!.recordCount, 1, reason: 'The error event is not counted'); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -278,6 +284,7 @@ void main() { analytics.send(testEvent); final logFileStats = analytics.logFileStats(); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -301,6 +308,7 @@ void main() { analytics.send(testEvent); expect(analytics.sentEvents, hasLength(1)); expect(logFile.readAsLinesSync(), hasLength(3)); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -309,6 +317,7 @@ void main() { // This will cause the first error analytics.logFileStats(); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -325,6 +334,7 @@ void main() { // This will cause the second error analytics.logFileStats(); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), @@ -333,6 +343,7 @@ void main() { // Attempting to cause the same error won't send another error event analytics.logFileStats(); + analytics.sendPendingErrorEvents(); expect( analytics.sentEvents.where( (element) => element.eventName == DashEvent.analyticsException), diff --git a/pkgs/unified_analytics/test/log_handler_test.dart b/pkgs/unified_analytics/test/log_handler_test.dart index a1f494a6e..bd2f89d6c 100644 --- a/pkgs/unified_analytics/test/log_handler_test.dart +++ b/pkgs/unified_analytics/test/log_handler_test.dart @@ -74,7 +74,7 @@ void main() { expect(analytics.logFileStats()!.recordCount, countOfEventsToSend); }); - test('The only record in the log file is malformed', () { + test('The only record in the log file is malformed', () async { // Write invalid json for the only log record logFile.writeAsStringSync('{{\n'); @@ -83,6 +83,9 @@ void main() { expect(logFileStats, isNull, reason: 'Null should be returned since only ' 'one record is in there and it is malformed'); + + analytics.sendPendingErrorEvents(); + expect( analytics.sentEvents, contains( @@ -126,6 +129,8 @@ void main() { expect(logFile.readAsLinesSync().length, countOfEventsToSend + countOfMalformedRecords); final logFileStats = analytics.logFileStats(); + + await analytics.close(); expect(logFile.readAsLinesSync().length, countOfEventsToSend + countOfMalformedRecords + 1, reason: @@ -172,6 +177,7 @@ void main() { analytics.send(testEvent); } final logFileStats = analytics.logFileStats(); + analytics.sendPendingErrorEvents(); expect(analytics.sentEvents.last.eventName, DashEvent.analyticsException, reason: 'Calling for the stats should have caused an error'); expect(logFile.readAsLinesSync().length, kLogFileLength); diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index c551a92b1..3d69a9a9a 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -145,6 +145,8 @@ void main() { analytics.userProperty.preparePayload(); expect(sessionFile.readAsStringSync(), '{"session_id": $timestamp}'); + analytics.sendPendingErrorEvents(); + // Attempting to fetch the session id when malformed should also // send an error event while parsing final lastEvent = analytics.sentEvents.last; @@ -184,6 +186,8 @@ void main() { // add additional work on startup analytics.send(testEvent); + analytics.close(); + final errorEvent = analytics.sentEvents .where((element) => element.eventName == DashEvent.analyticsException) .firstOrNull; @@ -209,6 +213,8 @@ void main() { analytics.userProperty.preparePayload(); expect(sessionFile.readAsStringSync(), '{"session_id": $timestamp}'); + analytics.close(); + // Attempting to fetch the session id when malformed should also // send an error event while parsing final lastEvent = analytics.sentEvents.last; From 75c560c9dfec7182bce61d60d144b477ad494edf Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:30:31 -0400 Subject: [PATCH 15/28] Fix test --- pkgs/unified_analytics/lib/src/analytics.dart | 2 +- pkgs/unified_analytics/test/log_handler_test.dart | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index a2d85ef7b..6f347f473 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -396,7 +396,7 @@ class AnalyticsImpl implements Analytics { /// user; this will return true if it is the first time the /// package is being used for a developer or if the consent /// message has been updated by the package. - late bool _showMessage; + bool _showMessage = false; /// When set to `true`, various assert statements will be enabled /// to ensure usage of this class is within GA4 limitations. diff --git a/pkgs/unified_analytics/test/log_handler_test.dart b/pkgs/unified_analytics/test/log_handler_test.dart index bd2f89d6c..3feff94f8 100644 --- a/pkgs/unified_analytics/test/log_handler_test.dart +++ b/pkgs/unified_analytics/test/log_handler_test.dart @@ -209,6 +209,7 @@ void main() { for (var i = 0; i < countOfEventsToSend; i++) { analytics.send(testEvent); } + analytics.sendPendingErrorEvents(); final secondLogFileStats = analytics.logFileStats(); expect(secondLogFileStats, isNotNull); From 37fd0dec833dd8a4f41f9335765d631df9a52f87 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 12:43:53 -0400 Subject: [PATCH 16/28] Make userprop variable private --- pkgs/unified_analytics/lib/src/analytics.dart | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 6f347f473..4f9414d44 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -384,13 +384,13 @@ abstract class Analytics { class AnalyticsImpl implements Analytics { final DashTool tool; final FileSystem fs; + final int toolsMessageVersion; final ConfigHandler _configHandler; final GAClient _gaClient; final SurveyHandler _surveyHandler; final File _clientIdFile; - final UserProperty userProperty; + final UserProperty _userProperty; final LogHandler _logHandler; - final int toolsMessageVersion; /// Tells the client if they need to show a message to the /// user; this will return true if it is the first time the @@ -447,7 +447,7 @@ class AnalyticsImpl implements Analytics { kDartToolDirectoryName, kClientIdFileName, )), - userProperty = UserProperty( + _userProperty = UserProperty( sessionFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, @@ -559,7 +559,7 @@ class AnalyticsImpl implements Analytics { @override Map> get userPropertyMap => - userProperty.preparePayload(); + _userProperty.preparePayload(); @override void clientShowedMessage() { @@ -672,7 +672,7 @@ class AnalyticsImpl implements Analytics { clientId: clientId, eventName: event.eventName, eventData: event.eventData, - userProperty: userProperty, + userProperty: _userProperty, ); if (_enableAsserts) checkBody(body); @@ -700,7 +700,7 @@ class AnalyticsImpl implements Analytics { // recreate the log file since it will only receives events // to persist from events sent Initializer.createClientIdFile(clientIdFile: _clientIdFile); - Initializer.createSessionFile(sessionFile: userProperty.sessionFile); + Initializer.createSessionFile(sessionFile: _userProperty.sessionFile); // Reread the client ID string so an empty string is not being // sent to GA4 since the persisted files are cleared when a user @@ -713,7 +713,7 @@ class AnalyticsImpl implements Analytics { clientId: clientId, eventName: collectionEvent.eventName, eventData: collectionEvent.eventData, - userProperty: userProperty, + userProperty: _userProperty, ); _logHandler.save(data: body); @@ -724,11 +724,11 @@ class AnalyticsImpl implements Analytics { clientId: clientId, eventName: collectionEvent.eventName, eventData: collectionEvent.eventData, - userProperty: userProperty, + userProperty: _userProperty, ); // For opted out users, data in the persisted files is cleared - userProperty.sessionFile.writeAsStringSync(''); + _userProperty.sessionFile.writeAsStringSync(''); _logHandler.logFile.writeAsStringSync(''); _clientIdFile.writeAsStringSync(''); @@ -775,7 +775,7 @@ class AnalyticsImpl implements Analytics { /// [FakeAnalytics.sentEvents]. void _sendPendingErrorEvents() { // Collect any errors encountered and send - final errorEvents = {...userProperty.errorSet, ..._logHandler.errorSet}; + final errorEvents = {..._userProperty.errorSet, ..._logHandler.errorSet}; errorEvents .where((event) => event.eventName == DashEvent.analyticsException && @@ -786,7 +786,7 @@ class AnalyticsImpl implements Analytics { _sentErrorEvents.addAll(errorEvents); // Clear error sets - userProperty.errorSet.clear(); + _userProperty.errorSet.clear(); _logHandler.errorSet.clear(); } } @@ -831,6 +831,9 @@ class FakeAnalytics extends AnalyticsImpl { ), ); + /// Getter to reference the private [UserProperty]. + UserProperty get userProperty => _userProperty; + @override void send(Event event) { if (!okToSend) return; @@ -840,7 +843,7 @@ class FakeAnalytics extends AnalyticsImpl { clientId: clientId, eventName: event.eventName, eventData: event.eventData, - userProperty: userProperty, + userProperty: _userProperty, ); if (_enableAsserts) checkBody(body); From f36692253fae2cc6adb2b2d7a329d1c5d5a971c5 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:23:27 -0400 Subject: [PATCH 17/28] Move configHandler to constructor initializer --- pkgs/unified_analytics/lib/src/analytics.dart | 46 ++++--------------- 1 file changed, 10 insertions(+), 36 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 4f9414d44..748e44036 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -24,9 +24,6 @@ import 'survey_handler.dart'; import 'user_property.dart'; import 'utils.dart'; -/// For passing the [Analytics.send] method to classes created by [Analytics]. -typedef SendFunction = void Function(Event event); - abstract class Analytics { /// The default factory constructor that will return an implementation /// of the [Analytics] abstract class using the [LocalFileSystem]. @@ -88,15 +85,6 @@ abstract class Analytics { homeDirectory: homeDirectory, toolsMessageVersion: kToolsMessageVersion, )..run(); - final configHandler = ConfigHandler( - fs: fs, - homeDirectory: homeDirectory, - configFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kConfigFileName, - )), - ); return AnalyticsImpl( tool: tool, @@ -120,7 +108,6 @@ abstract class Analytics { enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, - configHandler: configHandler, initializer: initializer, ); } @@ -184,15 +171,6 @@ abstract class Analytics { homeDirectory: homeDirectory, toolsMessageVersion: kToolsMessageVersion, )..run(); - final configHandler = ConfigHandler( - fs: fs, - homeDirectory: homeDirectory, - configFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kConfigFileName, - )), - ); return AnalyticsImpl( tool: tool, @@ -216,7 +194,6 @@ abstract class Analytics { enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, - configHandler: configHandler, initializer: initializer, ); } @@ -437,11 +414,9 @@ class AnalyticsImpl implements Analytics { required SurveyHandler surveyHandler, required bool enableAsserts, required Initializer initializer, - required ConfigHandler configHandler, }) : _gaClient = gaClient, _surveyHandler = surveyHandler, _enableAsserts = enableAsserts, - _configHandler = configHandler, _clientIdFile = fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, @@ -466,6 +441,15 @@ class AnalyticsImpl implements Analytics { clientIde: clientIde, enabledFeatures: enabledFeatures, ), + _configHandler = ConfigHandler( + fs: fs, + homeDirectory: homeDirectory, + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), + ), _logHandler = LogHandler( fs: fs, homeDirectory: homeDirectory, @@ -819,17 +803,7 @@ class FakeAnalytics extends AnalyticsImpl { super.toolsMessageVersion = kToolsMessageVersion, super.gaClient = const FakeGAClient(), super.enableAsserts = true, - }) : super( - configHandler: ConfigHandler( - fs: fs, - homeDirectory: homeDirectory, - configFile: fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kConfigFileName, - )), - ), - ); + }); /// Getter to reference the private [UserProperty]. UserProperty get userProperty => _userProperty; From 28807e42007c7930c4d5bbbebb7e0a44efaf1d53 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 13:48:31 -0400 Subject: [PATCH 18/28] Remove unnecessary parameters for log handler --- pkgs/unified_analytics/lib/src/analytics.dart | 2 -- pkgs/unified_analytics/lib/src/log_handler.dart | 8 +------- 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 748e44036..f42400c34 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -451,8 +451,6 @@ class AnalyticsImpl implements Analytics { )), ), _logHandler = LogHandler( - fs: fs, - homeDirectory: homeDirectory, logFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, diff --git a/pkgs/unified_analytics/lib/src/log_handler.dart b/pkgs/unified_analytics/lib/src/log_handler.dart index eafe36689..524aa1460 100644 --- a/pkgs/unified_analytics/lib/src/log_handler.dart +++ b/pkgs/unified_analytics/lib/src/log_handler.dart @@ -157,19 +157,13 @@ class LogFileStats { /// It will be treated as an append only log and will be limited /// to have has many data records as specified by [kLogFileLength]. class LogHandler { - final FileSystem fs; - final Directory homeDirectory; final File logFile; final Set errorSet = {}; /// A log handler constructor that will delegate saving /// logs and retrieving stats from the persisted log. - LogHandler({ - required this.fs, - required this.homeDirectory, - required this.logFile, - }); + LogHandler({required this.logFile}); /// Get stats from the persisted log file. /// From 20f31c05dbb694d9e76edd71f2c4c102fa2b418f Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:36:47 -0400 Subject: [PATCH 19/28] Add'l documentation --- pkgs/unified_analytics/lib/src/analytics.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index f42400c34..f6c8cdd71 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -829,6 +829,9 @@ class FakeAnalytics extends AnalyticsImpl { /// Public instance method to invoke private method that sends any /// pending error events. + /// + /// If this is never invoked, any pending error events will be sent + /// when invoking the [close] method. void sendPendingErrorEvents() => _sendPendingErrorEvents(); } From 227d5f528b047a40c433456fdc0f0f13b8a272c6 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 14:43:35 -0400 Subject: [PATCH 20/28] Remove unused params for survey handler --- .../example/serving_surveys.dart | 2 - pkgs/unified_analytics/lib/src/analytics.dart | 6 --- .../lib/src/survey_handler.dart | 10 +--- .../test/events_with_fake_test.dart | 2 - .../test/log_handler_test.dart | 3 +- .../test/survey_handler_test.dart | 49 ++----------------- .../test/unified_analytics_test.dart | 5 +- 7 files changed, 7 insertions(+), 70 deletions(-) diff --git a/pkgs/unified_analytics/example/serving_surveys.dart b/pkgs/unified_analytics/example/serving_surveys.dart index baacf20b8..e433f9026 100644 --- a/pkgs/unified_analytics/example/serving_surveys.dart +++ b/pkgs/unified_analytics/example/serving_surveys.dart @@ -58,8 +58,6 @@ void main() async { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: home, - fs: fs, dismissedSurveyFile: fs.file(p.join( home.path, kDartToolDirectoryName, diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index f6c8cdd71..43319de20 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -97,8 +97,6 @@ abstract class Analytics { fs: fs, gaClient: gaClient, surveyHandler: SurveyHandler( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, @@ -183,8 +181,6 @@ abstract class Analytics { fs: fs, gaClient: gaClient, surveyHandler: SurveyHandler( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, @@ -236,8 +232,6 @@ abstract class Analytics { fs: fs, surveyHandler: surveyHandler ?? FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, diff --git a/pkgs/unified_analytics/lib/src/survey_handler.dart b/pkgs/unified_analytics/lib/src/survey_handler.dart index fb4085203..effc96271 100644 --- a/pkgs/unified_analytics/lib/src/survey_handler.dart +++ b/pkgs/unified_analytics/lib/src/survey_handler.dart @@ -191,11 +191,7 @@ class SurveyButton { class SurveyHandler { final File dismissedSurveyFile; - SurveyHandler({ - required Directory homeDirectory, - required FileSystem fs, - required this.dismissedSurveyFile, - }); + SurveyHandler({required this.dismissedSurveyFile}); /// Invoking this method will persist the survey's id in /// the local file with either a snooze or permanently dismissed @@ -338,8 +334,6 @@ class FakeSurveyHandler extends SurveyHandler { /// will have their dates checked to ensure they are valid; it is /// recommended to use `package:clock` to set a fixed time for testing. FakeSurveyHandler.fromList({ - required super.homeDirectory, - required super.fs, required super.dismissedSurveyFile, required List initializedSurveys, }) { @@ -357,8 +351,6 @@ class FakeSurveyHandler extends SurveyHandler { /// Use this class in tests if you can provide raw /// json strings to simulate a response from a remote server. FakeSurveyHandler.fromString({ - required super.homeDirectory, - required super.fs, required super.dismissedSurveyFile, required String content, }) { diff --git a/pkgs/unified_analytics/test/events_with_fake_test.dart b/pkgs/unified_analytics/test/events_with_fake_test.dart index 8fda247cf..84be09fe1 100644 --- a/pkgs/unified_analytics/test/events_with_fake_test.dart +++ b/pkgs/unified_analytics/test/events_with_fake_test.dart @@ -83,8 +83,6 @@ void main() { fs: fs, toolsMessageVersion: toolsMessageVersion, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [testSurvey], ), diff --git a/pkgs/unified_analytics/test/log_handler_test.dart b/pkgs/unified_analytics/test/log_handler_test.dart index 3feff94f8..fa5e15ff6 100644 --- a/pkgs/unified_analytics/test/log_handler_test.dart +++ b/pkgs/unified_analytics/test/log_handler_test.dart @@ -85,7 +85,6 @@ void main() { 'one record is in there and it is malformed'); analytics.sendPendingErrorEvents(); - expect( analytics.sentEvents, contains( @@ -130,7 +129,7 @@ void main() { countOfEventsToSend + countOfMalformedRecords); final logFileStats = analytics.logFileStats(); - await analytics.close(); + analytics.sendPendingErrorEvents(); expect(logFile.readAsLinesSync().length, countOfEventsToSend + countOfMalformedRecords + 1, reason: diff --git a/pkgs/unified_analytics/test/survey_handler_test.dart b/pkgs/unified_analytics/test/survey_handler_test.dart index 11778fb42..d55ab0247 100644 --- a/pkgs/unified_analytics/test/survey_handler_test.dart +++ b/pkgs/unified_analytics/test/survey_handler_test.dart @@ -328,8 +328,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( @@ -383,8 +381,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( @@ -427,8 +423,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( @@ -474,10 +468,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, - fs: fs, - dismissedSurveyFile: dismissedSurveyFile, - content: ''' + dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "uniqueId123", @@ -579,10 +570,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, - fs: fs, - dismissedSurveyFile: dismissedSurveyFile, - content: ''' + dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "uniqueId123", @@ -646,10 +634,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, - fs: fs, - dismissedSurveyFile: dismissedSurveyFile, - content: ''' + dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "12345", @@ -732,8 +717,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( @@ -810,8 +793,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), @@ -857,8 +838,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), @@ -906,8 +885,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -937,8 +914,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -960,8 +935,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1008,8 +981,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1038,8 +1009,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1088,8 +1057,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1122,8 +1089,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1172,8 +1137,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1203,8 +1166,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), @@ -1229,8 +1190,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( @@ -1278,8 +1237,6 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 3d69a9a9a..555f6b5f0 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -186,8 +186,7 @@ void main() { // add additional work on startup analytics.send(testEvent); - analytics.close(); - + analytics.sendPendingErrorEvents(); final errorEvent = analytics.sentEvents .where((element) => element.eventName == DashEvent.analyticsException) .firstOrNull; @@ -213,7 +212,7 @@ void main() { analytics.userProperty.preparePayload(); expect(sessionFile.readAsStringSync(), '{"session_id": $timestamp}'); - analytics.close(); + analytics.sendPendingErrorEvents(); // Attempting to fetch the session id when malformed should also // send an error event while parsing From 9415f985106e750a821a0a6ccca9321c61625cc3 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 15:24:22 -0400 Subject: [PATCH 21/28] Changelog update + dartdocs --- pkgs/unified_analytics/CHANGELOG.md | 3 +++ pkgs/unified_analytics/lib/src/log_handler.dart | 2 ++ pkgs/unified_analytics/lib/src/user_property.dart | 3 +++ 3 files changed, 8 insertions(+) diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md index fd40259c2..364dd96c3 100644 --- a/pkgs/unified_analytics/CHANGELOG.md +++ b/pkgs/unified_analytics/CHANGELOG.md @@ -1,6 +1,9 @@ ## 5.8.6-wip - Refactored session handler class to use the last modified timestamp as the last ping value +- Consolidate `Session` functionality into `UserProperty` +- Remove `ErrorHandler` class and move its functionality/logic into `AnalyticsImpl` +- Get rid of `late` variables throughout implementation class, `AnalyticsImpl` ## 5.8.5 diff --git a/pkgs/unified_analytics/lib/src/log_handler.dart b/pkgs/unified_analytics/lib/src/log_handler.dart index 524aa1460..12f24d3a9 100644 --- a/pkgs/unified_analytics/lib/src/log_handler.dart +++ b/pkgs/unified_analytics/lib/src/log_handler.dart @@ -159,6 +159,8 @@ class LogFileStats { class LogHandler { final File logFile; + /// Contains instances of [Event.analyticsException] that were encountered + /// during a workflow and will be sent to GA4 for collection. final Set errorSet = {}; /// A log handler constructor that will delegate saving diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index e12117c62..ceff4a5fd 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -24,6 +24,9 @@ class UserProperty { final String? enabledFeatures; final File sessionFile; + + /// Contains instances of [Event.analyticsException] that were encountered + /// during a workflow and will be sent to GA4 for collection. final Set errorSet = {}; int? _sessionId; From 721a2c462a01004ee23a4fe07800d30385408e38 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Mon, 11 Mar 2024 16:25:31 -0400 Subject: [PATCH 22/28] Bump version to `6.0.0` --- pkgs/unified_analytics/CHANGELOG.md | 4 +++- pkgs/unified_analytics/lib/src/constants.dart | 2 +- pkgs/unified_analytics/pubspec.yaml | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md index 364dd96c3..9ba6ebd49 100644 --- a/pkgs/unified_analytics/CHANGELOG.md +++ b/pkgs/unified_analytics/CHANGELOG.md @@ -1,9 +1,11 @@ -## 5.8.6-wip +## 6.0.0-wip - Refactored session handler class to use the last modified timestamp as the last ping value - Consolidate `Session` functionality into `UserProperty` - Remove `ErrorHandler` class and move its functionality/logic into `AnalyticsImpl` - Get rid of `late` variables throughout implementation class, `AnalyticsImpl` +- Any error events (`Event.analyticsException`) encountered within package will be sent when invoking `Analytics.close` +- Exposing new method for `FakeAnalytics.sendPendingErrorEvents` to send error events on command ## 5.8.5 diff --git a/pkgs/unified_analytics/lib/src/constants.dart b/pkgs/unified_analytics/lib/src/constants.dart index e9034bbb1..41f7c6ecc 100644 --- a/pkgs/unified_analytics/lib/src/constants.dart +++ b/pkgs/unified_analytics/lib/src/constants.dart @@ -82,7 +82,7 @@ const int kLogFileLength = 2500; const String kLogFileName = 'dart-flutter-telemetry.log'; /// The current version of the package, should be in line with pubspec version. -const String kPackageVersion = '5.8.6-wip'; +const String kPackageVersion = '6.0.0-wip'; /// The minimum length for a session. const int kSessionDurationMinutes = 30; diff --git a/pkgs/unified_analytics/pubspec.yaml b/pkgs/unified_analytics/pubspec.yaml index 6c4f24809..ff1076381 100644 --- a/pkgs/unified_analytics/pubspec.yaml +++ b/pkgs/unified_analytics/pubspec.yaml @@ -4,7 +4,7 @@ description: >- to Google Analytics. # When updating this, keep the version consistent with the changelog and the # value in lib/src/constants.dart. -version: 5.8.6-wip +version: 6.0.0-wip repository: https://github.com/dart-lang/tools/tree/main/pkgs/unified_analytics environment: From a5fb177e34028eba8d81143d55ca264c40ad03fb Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Tue, 12 Mar 2024 08:04:28 -0400 Subject: [PATCH 23/28] Remove `Initializer` abstraction, sub for functions --- pkgs/unified_analytics/lib/src/analytics.dart | 39 +--- .../lib/src/config_handler.dart | 2 +- .../lib/src/initializer.dart | 221 ++++++++---------- .../lib/src/log_handler.dart | 2 +- .../lib/src/survey_handler.dart | 6 +- .../lib/src/user_property.dart | 4 +- .../test/events_with_fake_test.dart | 8 +- 7 files changed, 122 insertions(+), 160 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/analytics.dart b/pkgs/unified_analytics/lib/src/analytics.dart index 43319de20..da3fe0973 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -79,12 +79,7 @@ abstract class Analytics { apiSecret: kGoogleAnalyticsApiSecret, ); - final initializer = Initializer( - fs: fs, - tool: tool.label, - homeDirectory: homeDirectory, - toolsMessageVersion: kToolsMessageVersion, - )..run(); + final firstRun = runInitialization(homeDirectory: homeDirectory, fs: fs); return AnalyticsImpl( tool: tool, @@ -106,7 +101,7 @@ abstract class Analytics { enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, - initializer: initializer, + firstRun: firstRun, ); } @@ -163,12 +158,7 @@ abstract class Analytics { apiSecret: kTestApiSecret, ); - final initializer = Initializer( - fs: fs, - tool: tool.label, - homeDirectory: homeDirectory, - toolsMessageVersion: kToolsMessageVersion, - )..run(); + final firstRun = runInitialization(homeDirectory: homeDirectory, fs: fs); return AnalyticsImpl( tool: tool, @@ -190,7 +180,7 @@ abstract class Analytics { enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, - initializer: initializer, + firstRun: firstRun, ); } @@ -214,12 +204,7 @@ abstract class Analytics { int toolsMessageVersion = kToolsMessageVersion, String toolsMessage = kToolsMessage, }) { - final initializer = Initializer( - fs: fs, - tool: tool.label, - homeDirectory: homeDirectory, - toolsMessageVersion: toolsMessageVersion, - )..run(); + final firstRun = runInitialization(homeDirectory: homeDirectory, fs: fs); return FakeAnalytics( tool: tool, @@ -242,7 +227,7 @@ abstract class Analytics { gaClient: gaClient ?? const FakeGAClient(), clientIde: clientIde, enabledFeatures: enabledFeatures, - initializer: initializer, + firstRun: firstRun, ); } @@ -407,7 +392,7 @@ class AnalyticsImpl implements Analytics { required GAClient gaClient, required SurveyHandler surveyHandler, required bool enableAsserts, - required Initializer initializer, + required bool firstRun, }) : _gaClient = gaClient, _surveyHandler = surveyHandler, _enableAsserts = enableAsserts, @@ -458,7 +443,7 @@ class AnalyticsImpl implements Analytics { // This initializer class will let the instance know // if it was the first run; if it is, nothing will be sent // on the first run - if (initializer.firstRun) { + if (firstRun) { _showMessage = true; _firstRun = true; } else { @@ -487,7 +472,7 @@ class AnalyticsImpl implements Analytics { @override String get clientId { if (!_clientIdFile.existsSync()) { - Initializer.createClientIdFile(clientIdFile: _clientIdFile); + createClientIdFile(clientIdFile: _clientIdFile); } _clientId ??= _clientIdFile.readAsStringSync(); @@ -675,8 +660,8 @@ class AnalyticsImpl implements Analytics { // Recreate the session and client id file; no need to // recreate the log file since it will only receives events // to persist from events sent - Initializer.createClientIdFile(clientIdFile: _clientIdFile); - Initializer.createSessionFile(sessionFile: _userProperty.sessionFile); + createClientIdFile(clientIdFile: _clientIdFile); + createSessionFile(sessionFile: _userProperty.sessionFile); // Reread the client ID string so an empty string is not being // sent to GA4 since the persisted files are cleared when a user @@ -787,7 +772,7 @@ class FakeAnalytics extends AnalyticsImpl { required super.platform, required super.fs, required super.surveyHandler, - required super.initializer, + required super.firstRun, super.flutterChannel, super.flutterVersion, super.clientIde, diff --git a/pkgs/unified_analytics/lib/src/config_handler.dart b/pkgs/unified_analytics/lib/src/config_handler.dart index 75cd1e218..ea6f51ff6 100644 --- a/pkgs/unified_analytics/lib/src/config_handler.dart +++ b/pkgs/unified_analytics/lib/src/config_handler.dart @@ -175,7 +175,7 @@ class ConfigHandler { /// This will reset the configuration file and clear the /// [parsedTools] map and trigger parsing the config again. void resetConfig() { - Initializer.createConfigFile( + createConfigFile( configFile: fs.file(p.join( homeDirectory.path, kDartToolDirectoryName, diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 74bf55a37..1306d1962 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -9,135 +9,120 @@ import 'package:path/path.dart' as p; import 'constants.dart'; import 'utils.dart'; -class Initializer { - final FileSystem fs; - final String tool; - final Directory homeDirectory; - final int toolsMessageVersion; - bool firstRun = false; - - /// Responsibe for the initialization of the files - /// necessary for analytics reporting. - /// - /// Creates the configuration file that allows the user to - /// mannually opt out of reporting along with the file containing - /// the client ID to be used across all relevant tooling. - /// - /// Updating of the config file with new versions will - /// not be handled by the [Initializer]. - Initializer({ - required this.fs, - required this.tool, - required this.homeDirectory, - required this.toolsMessageVersion, - }); +/// Creates the text file that will contain the client ID +/// which will be used across all related tools for analytics +/// reporting in GA. +void createClientIdFile({required File clientIdFile}) { + clientIdFile.createSync(recursive: true); + clientIdFile.writeAsStringSync(Uuid().generateV4()); +} - /// Creates the text file that will contain the client ID - /// which will be used across all related tools for analytics - /// reporting in GA. - static void createClientIdFile({required File clientIdFile}) { - clientIdFile.createSync(recursive: true); - clientIdFile.writeAsStringSync(Uuid().generateV4()); +/// Creates the configuration file with the default message +/// in the user's home directory. +void createConfigFile({ + required File configFile, + required Directory homeDirectory, + required FileSystem fs, +}) { + configFile.createSync(recursive: true); + + // If the user was previously opted out, then we will + // replace the line that assumes automatic opt in with + // an opt out from the start + if (legacyOptOut(fs: fs, home: homeDirectory)) { + configFile.writeAsStringSync( + kConfigString.replaceAll('reporting=1', 'reporting=0')); + } else { + configFile.writeAsStringSync(kConfigString); } +} - /// Creates the configuration file with the default message - /// in the user's home directory. - static void createConfigFile({ - required File configFile, - required Directory homeDirectory, - required FileSystem fs, - }) { - configFile.createSync(recursive: true); +/// Creates that file that will persist dismissed survey ids. +void createDismissedSurveyFile({required File dismissedSurveyFile}) { + dismissedSurveyFile.createSync(recursive: true); + dismissedSurveyFile.writeAsStringSync('{}'); +} - // If the user was previously opted out, then we will - // replace the line that assumes automatic opt in with - // an opt out from the start - if (legacyOptOut(fs: fs, home: homeDirectory)) { - configFile.writeAsStringSync( - kConfigString.replaceAll('reporting=1', 'reporting=0')); - } else { - configFile.writeAsStringSync(kConfigString); - } - } +/// Creates that log file that will store the record formatted +/// events locally on the user's machine. +void createLogFile({required File logFile}) { + logFile.createSync(recursive: true); +} - /// Creates that file that will persist dismissed survey ids. - static void createDismissedSurveyFile({required File dismissedSurveyFile}) { - dismissedSurveyFile.createSync(recursive: true); - dismissedSurveyFile.writeAsStringSync('{}'); - } +/// Creates the session file which will contain +/// the current session id which is the current timestamp. +/// +/// [sessionIdOverride] can be provided as an override, otherwise it +/// will use the current timestamp from [Clock.now]. +void createSessionFile({ + required File sessionFile, + DateTime? sessionIdOverride, +}) { + final now = sessionIdOverride ?? clock.now(); + sessionFile.createSync(recursive: true); + sessionFile + .writeAsStringSync('{"session_id": ${now.millisecondsSinceEpoch}}'); + // TODO: eliasyishak, remove the below once https://github.com/google/file.dart/issues/236 is fixed + sessionFile.setLastModifiedSync(clock.now()); +} - /// Creates that log file that will store the record formatted - /// events locally on the user's machine. - static void createLogFile({required File logFile}) { - logFile.createSync(recursive: true); +/// Performs all of the initialization checks for the required files. +/// +/// Returns `true` if the config file was created indicating it is the first +/// time this package was run on a user's machine. +/// +/// Checks for the following: +/// - Config file +/// - Client ID file +/// - Session JSON file +/// - Log file +/// - Dismissed survey JSON file +bool runInitialization({ + required Directory homeDirectory, + required FileSystem fs, +}) { + var firstRun = false; + + // When the config file doesn't exist, initialize it with the default tools + // and the current date + final configFile = fs.file( + p.join(homeDirectory.path, kDartToolDirectoryName, kConfigFileName)); + if (!configFile.existsSync()) { + firstRun = true; + createConfigFile( + configFile: configFile, + fs: fs, + homeDirectory: homeDirectory, + ); } - /// Creates the session file which will contain - /// the current session id which is the current timestamp. - /// - /// [sessionIdOverride] can be provided as an override, otherwise it - /// will use the current timestamp from [Clock.now]. - static void createSessionFile({ - required File sessionFile, - DateTime? sessionIdOverride, - }) { - final now = sessionIdOverride ?? clock.now(); - sessionFile.createSync(recursive: true); - sessionFile - .writeAsStringSync('{"session_id": ${now.millisecondsSinceEpoch}}'); - // TODO: eliasyishak, remove the below once https://github.com/google/file.dart/issues/236 is fixed - sessionFile.setLastModifiedSync(clock.now()); + // Begin initialization checks for the client id + final clientFile = fs.file( + p.join(homeDirectory.path, kDartToolDirectoryName, kClientIdFileName)); + if (!clientFile.existsSync()) { + createClientIdFile(clientIdFile: clientFile); } - /// This will check that there is a client ID populated in - /// the user's home directory under the dart-tool directory. - /// If it doesn't exist, one will be created there. - /// - /// Passing [forceReset] as true will only reset the configuration - /// file, it won't recreate the client id, session, and log files - /// if they currently exist on disk. - void run({bool forceReset = false}) { - // Begin by checking for the config file - final configFile = fs.file( - p.join(homeDirectory.path, kDartToolDirectoryName, kConfigFileName)); - - // When the config file doesn't exist, initialize it with the default tools - // and the current date - if (!configFile.existsSync() || forceReset) { - firstRun = true; - createConfigFile( - configFile: configFile, - fs: fs, - homeDirectory: homeDirectory, - ); - } - - // Begin initialization checks for the client id - final clientFile = fs.file( - p.join(homeDirectory.path, kDartToolDirectoryName, kClientIdFileName)); - if (!clientFile.existsSync()) { - createClientIdFile(clientIdFile: clientFile); - } - - // Begin initialization checks for the session file - final sessionFile = fs.file( - p.join(homeDirectory.path, kDartToolDirectoryName, kSessionFileName)); - if (!sessionFile.existsSync()) { - createSessionFile(sessionFile: sessionFile); - } + // Begin initialization checks for the session file + final sessionFile = fs.file( + p.join(homeDirectory.path, kDartToolDirectoryName, kSessionFileName)); + if (!sessionFile.existsSync()) { + createSessionFile(sessionFile: sessionFile); + } - // Begin initialization checks for the log file to persist events locally - final logFile = fs - .file(p.join(homeDirectory.path, kDartToolDirectoryName, kLogFileName)); - if (!logFile.existsSync()) { - createLogFile(logFile: logFile); - } + // Begin initialization checks for the log file to persist events locally + final logFile = + fs.file(p.join(homeDirectory.path, kDartToolDirectoryName, kLogFileName)); + if (!logFile.existsSync()) { + createLogFile(logFile: logFile); + } - // Begin initialization checks for the dismissed survey file - final dismissedSurveyFile = fs.file(p.join( - homeDirectory.path, kDartToolDirectoryName, kDismissedSurveyFileName)); - if (!dismissedSurveyFile.existsSync()) { - createDismissedSurveyFile(dismissedSurveyFile: dismissedSurveyFile); - } + // Begin initialization checks for the dismissed survey file + final dismissedSurveyFile = fs.file(p.join( + homeDirectory.path, kDartToolDirectoryName, kDismissedSurveyFileName)); + if (!dismissedSurveyFile.existsSync()) { + createDismissedSurveyFile(dismissedSurveyFile: dismissedSurveyFile); } + + return firstRun; } diff --git a/pkgs/unified_analytics/lib/src/log_handler.dart b/pkgs/unified_analytics/lib/src/log_handler.dart index 12f24d3a9..6a0f05858 100644 --- a/pkgs/unified_analytics/lib/src/log_handler.dart +++ b/pkgs/unified_analytics/lib/src/log_handler.dart @@ -152,7 +152,7 @@ class LogFileStats { } /// This class is responsible for writing to a log -/// file that has been initialized by the [Initializer]. +/// file that has been initialized by the [createLogFile]. /// /// It will be treated as an append only log and will be limited /// to have has many data records as specified by [kLogFileLength]. diff --git a/pkgs/unified_analytics/lib/src/survey_handler.dart b/pkgs/unified_analytics/lib/src/survey_handler.dart index effc96271..cc533475e 100644 --- a/pkgs/unified_analytics/lib/src/survey_handler.dart +++ b/pkgs/unified_analytics/lib/src/survey_handler.dart @@ -279,12 +279,10 @@ class SurveyHandler { contents = jsonDecode(dismissedSurveyFile.readAsStringSync()) as Map; } on FormatException { - Initializer.createDismissedSurveyFile( - dismissedSurveyFile: dismissedSurveyFile); + createDismissedSurveyFile(dismissedSurveyFile: dismissedSurveyFile); contents = {}; } on FileSystemException { - Initializer.createDismissedSurveyFile( - dismissedSurveyFile: dismissedSurveyFile); + createDismissedSurveyFile(dismissedSurveyFile: dismissedSurveyFile); contents = {}; } diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index ceff4a5fd..fe6a54921 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -120,7 +120,7 @@ class UserProperty { parseContents(); } on FormatException catch (err) { final now = clock.now(); - Initializer.createSessionFile( + createSessionFile( sessionFile: sessionFile, sessionIdOverride: now, ); @@ -135,7 +135,7 @@ class UserProperty { _sessionId = now.millisecondsSinceEpoch; } on FileSystemException catch (err) { final now = clock.now(); - Initializer.createSessionFile( + createSessionFile( sessionFile: sessionFile, sessionIdOverride: now, ); diff --git a/pkgs/unified_analytics/test/events_with_fake_test.dart b/pkgs/unified_analytics/test/events_with_fake_test.dart index 84be09fe1..890d701ef 100644 --- a/pkgs/unified_analytics/test/events_with_fake_test.dart +++ b/pkgs/unified_analytics/test/events_with_fake_test.dart @@ -10,7 +10,6 @@ import 'package:test/test.dart'; import 'package:unified_analytics/src/constants.dart'; import 'package:unified_analytics/src/enums.dart'; -import 'package:unified_analytics/src/initializer.dart'; import 'package:unified_analytics/src/survey_handler.dart'; import 'package:unified_analytics/unified_analytics.dart'; @@ -86,12 +85,7 @@ void main() { dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [testSurvey], ), - initializer: Initializer( - fs: fs, - tool: DashTool.flutterTool.label, - homeDirectory: homeDirectory, - toolsMessageVersion: toolsMessageVersion, - ), + firstRun: false, ); }); }); From 30623bc17b201e386d473d5fe94f6eac2c2f1c55 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Tue, 12 Mar 2024 12:24:55 -0400 Subject: [PATCH 24/28] Fix test for session file modified timestamp --- pkgs/unified_analytics/lib/src/initializer.dart | 2 -- pkgs/unified_analytics/test/unified_analytics_test.dart | 8 ++++++++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 1306d1962..7a5e89646 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -62,8 +62,6 @@ void createSessionFile({ sessionFile.createSync(recursive: true); sessionFile .writeAsStringSync('{"session_id": ${now.millisecondsSinceEpoch}}'); - // TODO: eliasyishak, remove the below once https://github.com/google/file.dart/issues/236 is fixed - sessionFile.setLastModifiedSync(clock.now()); } /// Performs all of the initialization checks for the required files. diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 555f6b5f0..ff03e3aec 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -1045,6 +1045,14 @@ ${initialTool.label}=$dateStamp,$toolsMessageVersion test('Null values for flutter parameters is reflected properly in log file', () { + // Because we are using the `MemoryFileSystem.test` constructor, + // we don't have a real clock in the filesystem, and because we + // are checking the last modified timestamp for the session file + // to determine if we need to update the session id, manually setting + // that timestamp will ensure we are not updating session id when it + // first gets created + sessionFile.setLastModifiedSync(DateTime.now()); + // Use a for loop two initialize the second analytics instance // twice to account for no events being sent on the first instance // run for a given tool From 15950fa7442a5c711d28c5347f5daee8ba46e23c Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Tue, 12 Mar 2024 14:19:00 -0400 Subject: [PATCH 25/28] Remove `sessionIdOverride` for session initializer --- pkgs/unified_analytics/lib/src/initializer.dart | 13 ++++++------- pkgs/unified_analytics/lib/src/user_property.dart | 12 ++---------- 2 files changed, 8 insertions(+), 17 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 7a5e89646..c53276268 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -52,16 +52,15 @@ void createLogFile({required File logFile}) { /// Creates the session file which will contain /// the current session id which is the current timestamp. /// -/// [sessionIdOverride] can be provided as an override, otherwise it -/// will use the current timestamp from [Clock.now]. -void createSessionFile({ - required File sessionFile, - DateTime? sessionIdOverride, -}) { - final now = sessionIdOverride ?? clock.now(); +/// It also returns the timestamp used for the session if it needs +/// to be accessed. +DateTime createSessionFile({required File sessionFile}) { + final now = clock.now(); sessionFile.createSync(recursive: true); sessionFile .writeAsStringSync('{"session_id": ${now.millisecondsSinceEpoch}}'); + + return now; } /// Performs all of the initialization checks for the required files. diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index fe6a54921..99f6db944 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -119,11 +119,7 @@ class UserProperty { // being used as the session id and will get used to recreate the file parseContents(); } on FormatException catch (err) { - final now = clock.now(); - createSessionFile( - sessionFile: sessionFile, - sessionIdOverride: now, - ); + final now = createSessionFile(sessionFile: sessionFile); errorSet.add(Event.analyticsException( workflow: 'UserProperty._refreshSessionData', @@ -134,11 +130,7 @@ class UserProperty { // Fallback to setting the session id as the current time _sessionId = now.millisecondsSinceEpoch; } on FileSystemException catch (err) { - final now = clock.now(); - createSessionFile( - sessionFile: sessionFile, - sessionIdOverride: now, - ); + final now = createSessionFile(sessionFile: sessionFile); errorSet.add(Event.analyticsException( workflow: 'UserProperty._refreshSessionData', From 5b4ae3454a2ee907d30fda6ce83128b0bab9ac4f Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Tue, 12 Mar 2024 15:25:48 -0400 Subject: [PATCH 26/28] Clean up CHANGELOG --- pkgs/unified_analytics/CHANGELOG.md | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md index d2b4d8a81..7c1279215 100644 --- a/pkgs/unified_analytics/CHANGELOG.md +++ b/pkgs/unified_analytics/CHANGELOG.md @@ -1,10 +1,9 @@ ## 6.0.0-wip -- Refactored session handler class to use the last modified timestamp as the last ping value -- Consolidate `Session` functionality into `UserProperty` -- Remove `ErrorHandler` class and move its functionality/logic into `AnalyticsImpl` +- Refactored session handler class to use the last modified timestamp as the last ping value to prevent writing to file with each send +- Consolidate `Session` functionality into `UserProperty` to prevent race condition crash where session logic crashed before initializing `UserProperty` - Get rid of `late` variables throughout implementation class, `AnalyticsImpl` -- Any error events (`Event.analyticsException`) encountered within package will be sent when invoking `Analytics.close` +- Any error events (`Event.analyticsException`) encountered within package will be sent when invoking `Analytics.close`; replacing `ErrorHandler` functionality - Exposing new method for `FakeAnalytics.sendPendingErrorEvents` to send error events on command - Bumping intl package to 0.19.0 to fix version solving issue with flutter_tools From 37d29d7fcd86e4a5ea322abe67a85f0254f07df8 Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:56:35 -0400 Subject: [PATCH 27/28] Fix failing test --- pkgs/unified_analytics/lib/src/initializer.dart | 3 +-- pkgs/unified_analytics/lib/src/user_property.dart | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 748b37e6e..052e544f0 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -57,8 +57,7 @@ void createLogFile({required File logFile}) { DateTime createSessionFile({required File sessionFile}) { final now = clock.now(); sessionFile.createSync(recursive: true); - sessionFile - .writeAsStringSync('{"session_id": ${now.millisecondsSinceEpoch}}'); + writeSessionContents(sessionFile: sessionFile); return now; } diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index 99f6db944..d1a8bc071 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -67,7 +67,7 @@ class UserProperty { if (now.difference(lastPingDateTime).inMinutes > kSessionDurationMinutes) { // Update the session file with the latest session id _sessionId = now.millisecondsSinceEpoch; - sessionFile.writeAsStringSync('{"session_id": $_sessionId}'); + writeSessionContents(sessionFile: sessionFile); } else { // Update the last modified timestamp with the current timestamp so that // we can use it for the next _lastPing calculation From 75ec878f7d069d55d6865e7f261fd07dbe87147d Mon Sep 17 00:00:00 2001 From: eliasyishak <42216813+eliasyishak@users.noreply.github.com> Date: Fri, 15 Mar 2024 09:57:20 -0400 Subject: [PATCH 28/28] Format fix --- pkgs/unified_analytics/lib/src/initializer.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/unified_analytics/lib/src/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index 052e544f0..650388ba3 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -121,4 +121,4 @@ bool runInitialization({ } return firstRun; -} \ No newline at end of file +}