| // 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 'event.dart'; |
| import 'initializer.dart'; |
| import 'utils.dart'; |
| |
| class UserProperty { |
| final String? flutterChannel; |
| final String host; |
| final String? flutterVersion; |
| final String dartVersion; |
| final String tool; |
| final String hostOsVersion; |
| final String locale; |
| 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<Event> 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.flutterChannel, |
| required this.host, |
| required this.flutterVersion, |
| required this.dartVersion, |
| required this.tool, |
| required this.hostOsVersion, |
| 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 [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<String, Map<String, Object?>> preparePayload() { |
| return <String, Map<String, Object?>>{ |
| for (MapEntry<String, Object?> entry in _toMap().entries) |
| entry.key: <String, Object?>{'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 |
| /// 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<String, Object?>; |
| _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 [UserProperty] class. |
| Map<String, Object?> _toMap() => <String, Object?>{ |
| '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, |
| }; |
| } |