diff --git a/pkgs/unified_analytics/CHANGELOG.md b/pkgs/unified_analytics/CHANGELOG.md index c89094b814..1c827f0a78 100644 --- a/pkgs/unified_analytics/CHANGELOG.md +++ b/pkgs/unified_analytics/CHANGELOG.md @@ -1,3 +1,10 @@ +## 6.0.0-wip + +- 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`; replacing `ErrorHandler` functionality +- Exposing new method for `FakeAnalytics.sendPendingErrorEvents` to send error events on command + ## 5.8.8 - [Bug fix](https://github.com/dart-lang/tools/issues/252) rewrite the other call site for the session file @@ -8,7 +15,7 @@ ## 5.8.6 -- Refactored session handler class to use the last modified timestamp as the last ping value +- Refactored session handler class to use the last modified timestamp as the last ping value to prevent writing to file with each send - Bumping intl package to 0.19.0 to fix version solving issue with flutter_tools ## 5.8.5 diff --git a/pkgs/unified_analytics/example/serving_surveys.dart b/pkgs/unified_analytics/example/serving_surveys.dart index 18631b32aa..e433f9026a 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'; @@ -57,8 +58,11 @@ void main() async { fs: fs, platform: DevicePlatform.macos, 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 9187d02a52..da3fe0973b 100644 --- a/pkgs/unified_analytics/lib/src/analytics.dart +++ b/pkgs/unified_analytics/lib/src/analytics.dart @@ -16,19 +16,14 @@ 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'; import 'log_handler.dart'; -import 'session.dart'; 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]. @@ -84,6 +79,8 @@ abstract class Analytics { apiSecret: kGoogleAnalyticsApiSecret, ); + final firstRun = runInitialization(homeDirectory: homeDirectory, fs: fs); + return AnalyticsImpl( tool: tool, homeDirectory: homeDirectory, @@ -94,10 +91,17 @@ abstract class Analytics { toolsMessageVersion: kToolsMessageVersion, fs: fs, gaClient: gaClient, - surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs), + surveyHandler: SurveyHandler( + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), + ), enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, + firstRun: firstRun, ); } @@ -154,6 +158,8 @@ abstract class Analytics { apiSecret: kTestApiSecret, ); + final firstRun = runInitialization(homeDirectory: homeDirectory, fs: fs); + return AnalyticsImpl( tool: tool, homeDirectory: homeDirectory, @@ -164,10 +170,17 @@ abstract class Analytics { toolsMessageVersion: kToolsMessageVersion, fs: fs, gaClient: gaClient, - surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs), + surveyHandler: SurveyHandler( + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), + ), enableAsserts: enableAsserts, clientIde: clientIde, enabledFeatures: enabledFeatures, + firstRun: firstRun, ); } @@ -190,26 +203,33 @@ 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, - initializedSurveys: [], - ), - gaClient: gaClient ?? const FakeGAClient(), - clientIde: clientIde, - enabledFeatures: enabledFeatures, - ); + }) { + final firstRun = runInitialization(homeDirectory: homeDirectory, fs: fs); + + return FakeAnalytics( + tool: tool, + homeDirectory: homeDirectory, + flutterChannel: flutterChannel, + toolsMessageVersion: toolsMessageVersion, + flutterVersion: flutterVersion, + dartVersion: dartVersion, + platform: platform, + fs: fs, + surveyHandler: surveyHandler ?? + FakeSurveyHandler.fromList( + dismissedSurveyFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kDismissedSurveyFileName, + )), + initializedSurveys: [], + ), + gaClient: gaClient ?? const FakeGAClient(), + clientIde: clientIde, + enabledFeatures: enabledFeatures, + firstRun: firstRun, + ); + } /// The shared identifier for Flutter and Dart related tooling using /// package:unified_analytics. @@ -320,22 +340,19 @@ abstract class Analytics { class AnalyticsImpl implements Analytics { final DashTool tool; final FileSystem fs; - late final ConfigHandler _configHandler; + final int toolsMessageVersion; + final ConfigHandler _configHandler; final GAClient _gaClient; final SurveyHandler _surveyHandler; - late String _clientId; - late final File _clientIdFile; - late final UserProperty userProperty; - late final LogHandler _logHandler; - late final Session _sessionHandler; - late final ErrorHandler _errorHandler; - final int toolsMessageVersion; + final File _clientIdFile; + final UserProperty _userProperty; + final LogHandler _logHandler; /// Tells the client if they need to show a message to the /// 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. @@ -351,6 +368,16 @@ class AnalyticsImpl implements Analytics { /// from the [GAClient]. final _futures = >[]; + /// 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, @@ -365,9 +392,50 @@ class AnalyticsImpl implements Analytics { required GAClient gaClient, required SurveyHandler surveyHandler, required bool enableAsserts, + required bool firstRun, }) : _gaClient = gaClient, _surveyHandler = surveyHandler, - _enableAsserts = enableAsserts { + _enableAsserts = enableAsserts, + _clientIdFile = fs.file(p.join( + 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, + ), + _configHandler = ConfigHandler( + fs: fs, + homeDirectory: homeDirectory, + configFile: fs.file(p.join( + homeDirectory.path, + kDartToolDirectoryName, + kConfigFileName, + )), + ), + _logHandler = LogHandler( + 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(); @@ -375,14 +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 - final initializer = Initializer( - fs: fs, - tool: tool.label, - homeDirectory: homeDirectory, - toolsMessageVersion: toolsMessageVersion, - ); - initializer.run(); - if (initializer.firstRun) { + if (firstRun) { _showMessage = true; _firstRun = true; } else { @@ -390,13 +451,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, - ); - // 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 @@ -413,54 +467,17 @@ class AnalyticsImpl implements Analytics { // will be blocked _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); - - // 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] - _sessionHandler = Session( - homeDirectory: homeDirectory, - fs: fs, - errorHandler: _errorHandler, - ); - userProperty = UserProperty( - session: _sessionHandler, - 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, - ); - - // Initialize the log handler to persist events that are being sent - _logHandler = LogHandler( - fs: fs, - homeDirectory: homeDirectory, - errorHandler: _errorHandler, - ); - - // Initialize the session handler with the session_id - // by parsing the json file - _sessionHandler.initialize(telemetryEnabled); } @override - String get clientId => _clientId; + String get clientId { + if (!_clientIdFile.existsSync()) { + createClientIdFile(clientIdFile: _clientIdFile); + } + _clientId ??= _clientIdFile.readAsStringSync(); + + return _clientId!; + } @override String get getConsentMessage { @@ -503,7 +520,7 @@ class AnalyticsImpl implements Analytics { @override Map> get userPropertyMap => - userProperty.preparePayload(); + _userProperty.preparePayload(); @override void clientShowedMessage() { @@ -529,6 +546,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: () => [], @@ -555,7 +575,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; } @@ -610,10 +630,10 @@ 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, + userProperty: _userProperty, ); if (_enableAsserts) checkBody(body); @@ -640,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: _sessionHandler.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 @@ -651,10 +671,10 @@ 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, + userProperty: _userProperty, ); _logHandler.save(data: body); @@ -662,18 +682,18 @@ 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, + userProperty: _userProperty, ); // For opted out users, data in the persisted files is cleared - _sessionHandler.sessionFile.writeAsStringSync(''); + _userProperty.sessionFile.writeAsStringSync(''); _logHandler.logFile.writeAsStringSync(''); _clientIdFile.writeAsStringSync(''); - _clientId = _clientIdFile.readAsStringSync(); + _clientId = ''; } // Pass to the google analytics client to send with a @@ -706,6 +726,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 @@ -728,17 +772,18 @@ class FakeAnalytics extends AnalyticsImpl { required super.platform, required super.fs, required super.surveyHandler, + required super.firstRun, super.flutterChannel, super.flutterVersion, super.clientIde, super.enabledFeatures, - int? toolsMessageVersion, - GAClient? gaClient, - }) : super( - gaClient: gaClient ?? const FakeGAClient(), - enableAsserts: true, - toolsMessageVersion: toolsMessageVersion ?? kToolsMessageVersion, - ); + super.toolsMessageVersion = kToolsMessageVersion, + super.gaClient = const FakeGAClient(), + super.enableAsserts = true, + }); + + /// Getter to reference the private [UserProperty]. + UserProperty get userProperty => _userProperty; @override void send(Event event) { @@ -746,10 +791,10 @@ 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, + userProperty: _userProperty, ); if (_enableAsserts) checkBody(body); @@ -760,6 +805,13 @@ 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. + /// + /// If this is never invoked, any pending error events will be sent + /// when invoking the [close] method. + void sendPendingErrorEvents() => _sendPendingErrorEvents(); } /// An implementation that will never send events. diff --git a/pkgs/unified_analytics/lib/src/config_handler.dart b/pkgs/unified_analytics/lib/src/config_handler.dart index f9779dfabc..ea6f51ff6c 100644 --- a/pkgs/unified_analytics/lib/src/config_handler.dart +++ b/pkgs/unified_analytics/lib/src/config_handler.dart @@ -35,12 +35,11 @@ class ConfigHandler { final FileSystem fs; final Directory homeDirectory; - final Initializer initializer; final File configFile; final Map parsedTools = {}; - late DateTime configFileLastModified; + DateTime configFileLastModified; /// Reporting enabled unless specified by user bool _telemetryEnabled = true; @@ -48,16 +47,8 @@ class ConfigHandler { 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(); @@ -184,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); + 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/constants.dart b/pkgs/unified_analytics/lib/src/constants.dart index 9dd56812f3..41f7c6ecce 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.8'; +const String kPackageVersion = '6.0.0-wip'; /// The minimum length for a session. const int kSessionDurationMinutes = 30; 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 5937db93a0..0000000000 --- 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/initializer.dart b/pkgs/unified_analytics/lib/src/initializer.dart index a4d453a69c..650388ba33 100644 --- a/pkgs/unified_analytics/lib/src/initializer.dart +++ b/pkgs/unified_analytics/lib/src/initializer.dart @@ -9,133 +9,116 @@ 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. - static 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. +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 that file that will persist dismissed survey ids. +void createDismissedSurveyFile({required File dismissedSurveyFile}) { + dismissedSurveyFile.createSync(recursive: true); + dismissedSurveyFile.writeAsStringSync('{}'); +} - /// Creates the configuration file with the default message - /// in the user's home directory. - void createConfigFile({ - required File configFile, - required String dateStamp, - required String tool, - required int toolsMessageVersion, - }) { - 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 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 the session file which will contain +/// the current session id which is the current timestamp. +/// +/// 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); + writeSessionContents(sessionFile: sessionFile); + + return now; +} + +/// 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 that file that will persist dismissed survey ids. - static void createDismissedSurveyFile({required File dismissedSurveyFile}) { - dismissedSurveyFile.createSync(recursive: true); - dismissedSurveyFile.writeAsStringSync('{}'); + // Begin initialization checks for the client id + final clientFile = fs.file( + p.join(homeDirectory.path, kDartToolDirectoryName, kClientIdFileName)); + if (!clientFile.existsSync()) { + createClientIdFile(clientIdFile: clientFile); } - /// 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); + // Begin initialization checks for the session file + final sessionFile = fs.file( + p.join(homeDirectory.path, kDartToolDirectoryName, kSessionFileName)); + if (!sessionFile.existsSync()) { + createSessionFile(sessionFile: sessionFile); } - /// 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, - }) { - sessionFile.createSync(recursive: true); - writeSessionContents(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); } - /// 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, - dateStamp: dateStamp, - tool: tool, - toolsMessageVersion: toolsMessageVersion, - ); - } - - // 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 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 c406cce944..6a0f058581 100644 --- a/pkgs/unified_analytics/lib/src/log_handler.dart +++ b/pkgs/unified_analytics/lib/src/log_handler.dart @@ -6,10 +6,8 @@ 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'; import 'event.dart'; import 'initializer.dart'; @@ -154,28 +152,20 @@ 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]. class LogHandler { - final FileSystem fs; - final Directory homeDirectory; final File logFile; - final ErrorHandler _errorHandler; + + /// 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 /// logs and retrieving stats from the persisted log. - LogHandler({ - required this.fs, - required this.homeDirectory, - required ErrorHandler errorHandler, - }) : logFile = fs.file(p.join( - homeDirectory.path, - kDartToolDirectoryName, - kLogFileName, - )), - _errorHandler = errorHandler; + LogHandler({required this.logFile}); /// Get stats from the persisted log file. /// @@ -192,7 +182,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}', @@ -201,7 +191,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/session.dart b/pkgs/unified_analytics/lib/src/session.dart deleted file mode 100644 index 540f1fdbd0..0000000000 --- a/pkgs/unified_analytics/lib/src/session.dart +++ /dev/null @@ -1,126 +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 'package:path/path.dart' as p; - -import 'constants.dart'; -import 'error_handler.dart'; -import 'event.dart'; -import 'initializer.dart'; -import 'utils.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, - }) : sessionFile = fs.file(p.join( - homeDirectory.path, kDartToolDirectoryName, kSessionFileName)), - _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; - writeSessionContents(sessionFile: sessionFile); - } 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/survey_handler.dart b/pkgs/unified_analytics/lib/src/survey_handler.dart index 4925601127..cc533475e2 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,9 @@ 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, - )); + SurveyHandler({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 +219,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 +276,13 @@ class SurveyHandler { Map _parseJsonFile() { Map contents; try { - contents = jsonDecode(_dismissedSurveyFile.readAsStringSync()) + 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 = {}; } @@ -342,8 +332,7 @@ 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, }) { // We must pass the surveys from the list to the @@ -360,8 +349,7 @@ 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, }) { final body = jsonDecode(content) as List; diff --git a/pkgs/unified_analytics/lib/src/user_property.dart b/pkgs/unified_analytics/lib/src/user_property.dart index 33d0e475f4..d1a8bc0717 100644 --- a/pkgs/unified_analytics/lib/src/user_property.dart +++ b/pkgs/unified_analytics/lib/src/user_property.dart @@ -5,13 +5,14 @@ import 'dart:convert'; import 'package:clock/clock.dart'; +import 'package:file/file.dart'; import 'constants.dart'; -import 'session.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 +23,18 @@ class UserProperty { final String? clientIde; 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; + /// 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 +44,45 @@ class UserProperty { required this.locale, required this.clientIde, required this.enabledFeatures, + required this.sessionFile, }); + /// 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; + writeSessionContents(sessionFile: sessionFile); + } 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; + } + /// 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() { @@ -57,10 +97,56 @@ class UserProperty { 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 + /// 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 = createSessionFile(sessionFile: sessionFile); + + errorSet.add(Event.analyticsException( + workflow: 'UserProperty._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 = createSessionFile(sessionFile: sessionFile); + + errorSet.add(Event.analyticsException( + workflow: 'UserProperty._refreshSessionData', + error: err.runtimeType.toString(), + description: err.osError?.toString(), + )); + + // Fallback to setting the session id as the current time + _sessionId = now.millisecondsSinceEpoch; + } + } + /// 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, diff --git a/pkgs/unified_analytics/pubspec.yaml b/pkgs/unified_analytics/pubspec.yaml index 9788d8f13a..02ac8d2edf 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.8 +version: 6.0.0-wip repository: https://github.com/dart-lang/tools/tree/main/pkgs/unified_analytics environment: diff --git a/pkgs/unified_analytics/test/error_handler_test.dart b/pkgs/unified_analytics/test/error_handler_test.dart index c7156e2aaf..22decf72d7 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/events_with_fake_test.dart b/pkgs/unified_analytics/test/events_with_fake_test.dart index 2a07c1656c..890d701ef6 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, @@ -65,17 +73,19 @@ 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], ), + firstRun: false, ); }); }); diff --git a/pkgs/unified_analytics/test/log_handler_test.dart b/pkgs/unified_analytics/test/log_handler_test.dart index a1f494a6ee..fa5e15ff6e 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,8 @@ 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 +128,8 @@ void main() { expect(logFile.readAsLinesSync().length, countOfEventsToSend + countOfMalformedRecords); final logFileStats = analytics.logFileStats(); + + analytics.sendPendingErrorEvents(); expect(logFile.readAsLinesSync().length, countOfEventsToSend + countOfMalformedRecords + 1, reason: @@ -172,6 +176,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); @@ -203,6 +208,7 @@ void main() { for (var i = 0; i < countOfEventsToSend; i++) { analytics.send(testEvent); } + analytics.sendPendingErrorEvents(); final secondLogFileStats = analytics.logFileStats(); expect(secondLogFileStats, isNotNull); diff --git a/pkgs/unified_analytics/test/survey_handler_test.dart b/pkgs/unified_analytics/test/survey_handler_test.dart index 908a654c55..d55ab02471 100644 --- a/pkgs/unified_analytics/test/survey_handler_test.dart +++ b/pkgs/unified_analytics/test/survey_handler_test.dart @@ -328,8 +328,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -382,8 +381,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -425,8 +423,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -471,7 +468,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, fs: fs, content: ''' + dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "uniqueId123", @@ -573,7 +570,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, fs: fs, content: ''' + dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "uniqueId123", @@ -637,7 +634,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromString( - homeDirectory: homeDirectory, fs: fs, content: ''' + dismissedSurveyFile: dismissedSurveyFile, content: ''' [ { "uniqueId": "12345", @@ -720,8 +717,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -797,8 +793,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), ); @@ -843,8 +838,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [survey], ), ); @@ -891,8 +885,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -921,8 +914,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -943,8 +935,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -990,8 +981,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1019,8 +1009,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1068,8 +1057,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1101,8 +1089,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1150,8 +1137,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1180,8 +1166,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [surveyToLoad], ), ); @@ -1205,8 +1190,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', @@ -1253,8 +1237,7 @@ void main() { fs: fs, platform: DevicePlatform.macos, surveyHandler: FakeSurveyHandler.fromList( - homeDirectory: homeDirectory, - fs: fs, + dismissedSurveyFile: dismissedSurveyFile, initializedSurveys: [ Survey( uniqueId: 'uniqueId', diff --git a/pkgs/unified_analytics/test/unified_analytics_test.dart b/pkgs/unified_analytics/test/unified_analytics_test.dart index 55d20010c6..e466c80fa7 100644 --- a/pkgs/unified_analytics/test/unified_analytics_test.dart +++ b/pkgs/unified_analytics/test/unified_analytics_test.dart @@ -146,12 +146,15 @@ void main() { expect(sessionFile.readAsStringSync(), '{"session_id": $timestamp, "last_ping": $timestamp}'); + analytics.sendPendingErrorEvents(); + // Attempting to fetch the session id when malformed should also // send an error event while parsing 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'); }); }); @@ -178,11 +181,19 @@ 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); + + analytics.sendPendingErrorEvents(); final errorEvent = analytics.sentEvents .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'); @@ -203,12 +214,15 @@ void main() { expect(sessionFile.readAsStringSync(), '{"session_id": $timestamp, "last_ping": $timestamp}'); + analytics.sendPendingErrorEvents(); + // Attempting to fetch the session id when malformed should also // send an error event while parsing 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'); }); });