Skip to content

Commit

Permalink
Add user properties: host_os_version, locale, and client_ide (#195
Browse files Browse the repository at this point in the history
)

* Added 2 keys in `UserProperty` class for locale and os version

* Added `clientIde` as field for constructor

* Clean up failing tests

* Bump versions

* Truncating host os if necessary + tests for util function

* Fix analyze errors

* More information in example
  • Loading branch information
eliasyishak authored Nov 8, 2023
1 parent 6ad3691 commit d898ad1
Show file tree
Hide file tree
Showing 10 changed files with 177 additions and 14 deletions.
6 changes: 6 additions & 0 deletions pkgs/unified_analytics/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
## 5.3.0

- User property "host_os_version" added to provide detail version information about the host
- User property "locale" added to provide language related information
- User property "client_ide" (optional) added to provide the IDE used by the Dash tool using this package, if applicable

## 5.2.0

- Added the `Event.hotRunnerInfo` constructor
Expand Down
4 changes: 4 additions & 0 deletions pkgs/unified_analytics/example/unified_analytics_example.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,11 @@ final Analytics analytics = Analytics.development(
tool: DashTool.flutterTool,
flutterChannel: 'ey-test-channel',
flutterVersion: 'Flutter 3.6.0-7.0.pre.47',
clientIde: 'VSCode',
dartVersion: 'Dart 2.19.0',
// This can be set to true while testing to validate
// against GA4 usage limitations (character limits, etc.)
enableAsserts: false,
);

// Timing a process and sending the event
Expand Down
22 changes: 20 additions & 2 deletions pkgs/unified_analytics/lib/src/analytics.dart
Original file line number Diff line number Diff line change
Expand Up @@ -34,11 +34,16 @@ abstract class Analytics {
///
/// [flutterChannel] and [flutterVersion] are nullable in case the client
/// using this package is unable to resolve those values.
///
/// An optional parameter [clientIde] is also available for dart and flutter
/// tooling that are running from IDEs can be resolved. Such as "VSCode"
/// running the flutter-tool.
factory Analytics({
required DashTool tool,
required String dartVersion,
String? flutterChannel,
String? flutterVersion,
String? clientIde,
bool enableAsserts = false,
}) {
// Create the instance of the file system so clients don't need
Expand Down Expand Up @@ -81,6 +86,7 @@ abstract class Analytics {
gaClient: gaClient,
surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs),
enableAsserts: enableAsserts,
clientIde: clientIde,
);
}

Expand All @@ -98,6 +104,7 @@ abstract class Analytics {
required String dartVersion,
String? flutterChannel,
String? flutterVersion,
String? clientIde,
bool enableAsserts = true,
}) {
// Create the instance of the file system so clients don't need
Expand Down Expand Up @@ -147,6 +154,7 @@ abstract class Analytics {
gaClient: gaClient,
surveyHandler: SurveyHandler(homeDirectory: homeDirectory, fs: fs),
enableAsserts: enableAsserts,
clientIde: clientIde,
);
}

Expand All @@ -163,6 +171,7 @@ abstract class Analytics {
required DevicePlatform platform,
String? flutterChannel,
String? flutterVersion,
String? clientIde,
SurveyHandler? surveyHandler,
GAClient? gaClient,
int toolsMessageVersion = kToolsMessageVersion,
Expand All @@ -185,6 +194,7 @@ abstract class Analytics {
),
gaClient: gaClient ?? const FakeGAClient(),
enableAsserts: true,
clientIde: clientIde,
);

/// The shared identifier for Flutter and Dart related tooling using
Expand Down Expand Up @@ -339,8 +349,9 @@ class AnalyticsImpl implements Analytics {
AnalyticsImpl({
required this.tool,
required Directory homeDirectory,
String? flutterChannel,
String? flutterVersion,
required String? flutterChannel,
required String? flutterVersion,
required String? clientIde,
required String dartVersion,
required DevicePlatform platform,
required this.toolsMessageVersion,
Expand Down Expand Up @@ -409,6 +420,12 @@ class AnalyticsImpl implements Analytics {
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,
);

// Initialize the log handler to persist events that are being sent
Expand Down Expand Up @@ -680,6 +697,7 @@ class FakeAnalytics extends AnalyticsImpl {
required super.surveyHandler,
super.flutterChannel,
super.flutterVersion,
super.clientIde,
}) : super(
gaClient: const FakeGAClient(),
enableAsserts: true,
Expand Down
2 changes: 1 addition & 1 deletion pkgs/unified_analytics/lib/src/constants.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.0';
const String kPackageVersion = '5.3.0';

/// The minimum length for a session.
const int kSessionDurationMinutes = 30;
Expand Down
43 changes: 36 additions & 7 deletions pkgs/unified_analytics/lib/src/log_handler.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,9 @@ class LogItem {
final String dartVersion;
final String tool;
final DateTime localTime;
final String hostOsVersion;
final String locale;
final String? clientIde;

LogItem({
required this.eventName,
Expand All @@ -305,6 +308,9 @@ class LogItem {
required this.dartVersion,
required this.tool,
required this.localTime,
required this.hostOsVersion,
required this.locale,
required this.clientIde,
});

/// Serves a parser for each record in the log file.
Expand All @@ -319,18 +325,18 @@ class LogItem {
/// Example of what a record looks like:
/// ```
/// {
/// "client_id": "d40133a0-7ea6-4347-b668-ffae94bb8774",
/// "client_id": "ffcea97b-db5e-4c66-98c2-3942de4fac40",
/// "events": [
/// {
/// "name": "hot_reload_time",
/// "params": {
/// "time_ns": 345
/// "timeMs": 135
/// }
/// }
/// ],
/// "user_properties": {
/// "session_id": {
/// "value": 1675193534342
/// "value": 1699385899950
/// },
/// "flutter_channel": {
/// "value": "ey-test-channel"
Expand All @@ -344,11 +350,23 @@ class LogItem {
/// "dart_version": {
/// "value": "Dart 2.19.0"
/// },
/// "analytics_pkg_version": {
/// "value": "5.2.0"
/// },
/// "tool": {
/// "value": "flutter-tools"
/// "value": "flutter-tool"
/// },
/// "local_time": {
/// "value": "2023-01-31 14:32:14.592898 -0500"
/// "value": "2023-11-07 15:09:03.025559 -0500"
/// },
/// "host_os_version": {
/// "value": "Version 14.1 (Build 23B74)"
/// },
/// "locale": {
/// "value": "en"
/// },
/// "clientIde": {
/// "value": "VSCode"
/// }
/// }
/// }
Expand Down Expand Up @@ -387,10 +405,16 @@ class LogItem {
(userProps['tool']! as Map<String, Object?>)['value'] as String?;
final localTimeString = (userProps['local_time']!
as Map<String, Object?>)['value'] as String?;
final hostOsVersion = (userProps['host_os_version']!
as Map<String, Object?>)['value'] as String?;
final locale =
(userProps['locale']! as Map<String, Object?>)['value'] as String?;
final clientIde = (userProps['client_ide']!
as Map<String, Object?>)['value'] as String?;

// If any of the above values are null, return null since that
// indicates the record is malformed; note that `flutter_version`
// and `flutter_channel` are nullable fields in the log file
// indicates the record is malformed; note that `flutter_version`,
// `flutter_channel`, and `client_ide` are nullable fields in the log file
final values = <Object?>[
// Values associated with the top level key = 'events'
eventName,
Expand All @@ -401,6 +425,8 @@ class LogItem {
dartVersion,
tool,
localTimeString,
hostOsVersion,
locale,
];
for (var value in values) {
if (value == null) return null;
Expand All @@ -418,6 +444,9 @@ class LogItem {
dartVersion: dartVersion!,
tool: tool!,
localTime: localTime,
hostOsVersion: hostOsVersion!,
locale: locale!,
clientIde: clientIde,
);
// ignore: avoid_catching_errors
} on TypeError {
Expand Down
9 changes: 9 additions & 0 deletions pkgs/unified_analytics/lib/src/user_property.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ class UserProperty {
final String? flutterVersion;
final String dartVersion;
final String tool;
final String hostOsVersion;
final String locale;
final String? clientIde;

/// This class is intended to capture all of the user's
/// metadata when the class gets initialized as well as collecting
Expand All @@ -28,6 +31,9 @@ class UserProperty {
required this.flutterVersion,
required this.dartVersion,
required this.tool,
required this.hostOsVersion,
required this.locale,
required this.clientIde,
});

/// This method will take the data in this class and convert it into
Expand Down Expand Up @@ -60,5 +66,8 @@ class UserProperty {
'analytics_pkg_version': kPackageVersion,
'tool': tool,
'local_time': formatDateTime(clock.now()),
'host_os_version': hostOsVersion,
'locale': locale,
'client_ide': clientIde,
};
}
30 changes: 30 additions & 0 deletions pkgs/unified_analytics/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import 'package:intl/intl.dart';
import 'package:path/path.dart' as p;

import 'enums.dart';
import 'event.dart';
import 'survey_handler.dart';
import 'user_property.dart';

Expand Down Expand Up @@ -242,6 +243,35 @@ bool surveySnoozedOrDismissed(
return survey.snoozeForMinutes > minutesElapsed;
}

/// Due to some limitations for GA4, this function can be used to
/// truncate fields that we may not care about truncating, such as
/// the host os details.
///
/// [maxLength] represents the maximum length allowed for the string.
///
/// Example:
/// "Linux 6.2.0-1015-azure #15~22.04.1-Ubuntu SMP Fri Oct 6 13:20:44 UTC 2023"
///
/// The above string is what is returned by [io.Platform.operatingSystemVersion]
/// for certain machines running GitHub Actions, this function will truncate
/// that value down to the maximum length at 36 characters and return the below
///
/// Return:
/// "Linux 6.2.0-1015-azure #15~22."
///
/// This should only be used on fields that are okay to be truncated, this
/// should not be used for parameters on the [Event] constructors.
String truncateStringToLength(String str, int maxLength) {
if (maxLength <= 0) {
throw ArgumentError(
'The length to truncate a string must be greater than 0');
}

if (maxLength > str.length) return str;

return str.substring(0, maxLength);
}

/// A UUID generator.
///
/// This will generate unique IDs in the format:
Expand Down
2 changes: 1 addition & 1 deletion pkgs/unified_analytics/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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.2.0
version: 5.3.0
repository: https://github.com/dart-lang/tools/tree/main/pkgs/unified_analytics

environment:
Expand Down
63 changes: 60 additions & 3 deletions pkgs/unified_analytics/test/log_handler_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,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/utils.dart';
import 'package:unified_analytics/unified_analytics.dart';

void main() {
Expand Down Expand Up @@ -128,9 +129,9 @@ void main() {
// like the other malformed records, instead the LogItem.fromRecord
// constructor will return null if all the keys are not available
final contents = '''
{"client_id":"fe4a035b-bba8-4d4b-a651-ea213e9b8a2c","events":[{"name":"lint_usage_count","params":{"count":1,"name":"prefer_final_fields"}}],"user_properties":{"session_id":{"value":1695147041117},"flutter_channel":{"value":null},"host":{"value":"macOS"},"flutter_version":{"value":"3.14.0-14.0.pre.303"},"dart_version":{"value":"3.2.0-140.0.dev"},"analytics_pkg_version":{"value":"3.1.0"},"tool":{"value":"vscode-plugins"},"local_time":{"value":"2023-09-19 14:44:11.528153 -0400"}}}
{"client_id":"fe4a035b-bba8-4d4b-a651-ea213e9b8a2c","WRONG_EVENT_KEY":[{"name":"lint_usage_count","params":{"count":1,"name":"prefer_for_elements_to_map_fromIterable"}}],"user_properties":{"session_id":{"value":1695147041117},"flutter_channel":{"value":null},"host":{"value":"macOS"},"flutter_version":{"value":"3.14.0-14.0.pre.303"},"dart_version":{"value":"3.2.0-140.0.dev"},"analytics_pkg_version":{"value":"3.1.0"},"tool":{"value":"vscode-plugins"},"local_time":{"value":"2023-09-19 14:44:11.565549 -0400"}}}
{"client_id":"fe4a035b-bba8-4d4b-a651-ea213e9b8a2c","events":[{"name":"lint_usage_count","params":{"count":1,"name":"prefer_function_declarations_over_variables"}}],"user_properties":{"session_id":{"value":1695147041117},"flutter_channel":{"value":null},"host":{"value":"macOS"},"flutter_version":{"value":"3.14.0-14.0.pre.303"},"dart_version":{"value":"3.2.0-140.0.dev"},"analytics_pkg_version":{"value":"3.1.0"},"tool":{"value":"vscode-plugins"},"local_time":{"value":"2023-09-19 14:44:11.589338 -0400"}}}
{"client_id":"ffcea97b-db5e-4c66-98c2-3942de4fac40","events":[{"name":"hot_reload_time","params":{"timeMs":136}}],"user_properties":{"session_id":{"value":1699385899950},"flutter_channel":{"value":"ey-test-channel"},"host":{"value":"macOS"},"flutter_version":{"value":"Flutter 3.6.0-7.0.pre.47"},"dart_version":{"value":"Dart 2.19.0"},"analytics_pkg_version":{"value":"5.2.0"},"tool":{"value":"flutter-tool"},"local_time":{"value":"2023-11-07 15:37:26.685761 -0500"},"host_os_version":{"value":"Version 14.1 (Build 23B74)"},"locale":{"value":"en"},"client_ide":{"value":"VSCode"}}}
{"client_id":"ffcea97b-db5e-4c66-98c2-3942de4fac40","WRONG_EVENT_KEY":[{"name":"hot_reload_time","params":{"timeMs":136}}],"user_properties":{"session_id":{"value":1699385899950},"flutter_channel":{"value":"ey-test-channel"},"host":{"value":"macOS"},"flutter_version":{"value":"Flutter 3.6.0-7.0.pre.47"},"dart_version":{"value":"Dart 2.19.0"},"analytics_pkg_version":{"value":"5.2.0"},"tool":{"value":"flutter-tool"},"local_time":{"value":"2023-11-07 15:37:26.685761 -0500"},"host_os_version":{"value":"Version 14.1 (Build 23B74)"},"locale":{"value":"en"},"client_ide":{"value":"VSCode"}}}
{"client_id":"ffcea97b-db5e-4c66-98c2-3942de4fac40","events":[{"name":"hot_reload_time","params":{"timeMs":136}}],"user_properties":{"session_id":{"value":1699385899950},"flutter_channel":{"value":"ey-test-channel"},"host":{"value":"macOS"},"flutter_version":{"value":"Flutter 3.6.0-7.0.pre.47"},"dart_version":{"value":"Dart 2.19.0"},"analytics_pkg_version":{"value":"5.2.0"},"tool":{"value":"flutter-tool"},"local_time":{"value":"2023-11-07 15:37:26.685761 -0500"},"host_os_version":{"value":"Version 14.1 (Build 23B74)"},"locale":{"value":"en"},"client_ide":{"value":"VSCode"}}}
''';
logFile.writeAsStringSync(contents);

Expand Down Expand Up @@ -185,4 +186,60 @@ void main() {
expect(secondLogFileStats, isNotNull);
expect(secondLogFileStats!.recordCount, countOfEventsToSend);
});

test(
'truncateStringToLength returns same string when '
'max length greater than string length', () {
final testString = 'Version 14.1 (Build 23B74)';
final maxLength = 100;

expect(testString.length < maxLength, true);

String runTruncateString() => truncateStringToLength(testString, maxLength);

expect(runTruncateString, returnsNormally);

final newString = runTruncateString();
expect(newString, testString);
});

test(
'truncateStringToLength returns truncated string when '
'max length less than string length', () {
final testString = 'Version 14.1 (Build 23B74)';
final maxLength = 10;

expect(testString.length > maxLength, true);

String runTruncateString() => truncateStringToLength(testString, maxLength);

expect(runTruncateString, returnsNormally);

final newString = runTruncateString();
expect(newString.length, maxLength);
expect(newString, 'Version 14');
});

test('truncateStringToLength handle errors for invalid max length', () {
final testString = 'Version 14.1 (Build 23B74)';
var maxLength = 0;
String runTruncateString() => truncateStringToLength(testString, maxLength);

expect(runTruncateString, throwsArgumentError);

maxLength = -1;
expect(runTruncateString, throwsArgumentError);
});

test('truncateStringToLength same string when max length is the same', () {
final testString = 'Version 14.1 (Build 23B74)';
final maxLength = testString.length;

String runTruncateString() => truncateStringToLength(testString, maxLength);
expect(runTruncateString, returnsNormally);

final newString = runTruncateString();
expect(newString.length, maxLength);
expect(newString, testString);
});
}
Loading

0 comments on commit d898ad1

Please sign in to comment.