| // 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 'dart:io' as io; |
| import 'dart:math' show Random; |
| |
| import 'package:file/file.dart'; |
| import 'package:path/path.dart' as p; |
| |
| import 'enums.dart'; |
| import 'user_property.dart'; |
| |
| /// Format time as 'yyyy-MM-dd HH:mm:ss Z' where Z is the difference between the |
| /// timezone of t and UTC formatted according to RFC 822. |
| String formatDateTime(DateTime t) { |
| final String sign = t.timeZoneOffset.isNegative ? '-' : '+'; |
| final Duration tzOffset = t.timeZoneOffset.abs(); |
| final int hoursOffset = tzOffset.inHours; |
| final int minutesOffset = |
| tzOffset.inMinutes - (Duration.minutesPerHour * hoursOffset); |
| assert(hoursOffset < 24); |
| assert(minutesOffset < 60); |
| |
| String twoDigits(int n) => (n >= 10) ? '$n' : '0$n'; |
| return '$t $sign${twoDigits(hoursOffset)}${twoDigits(minutesOffset)}'; |
| } |
| |
| /// Construct the Map that will be converted to json for the |
| /// body of the request |
| /// |
| /// Follows the following schema |
| /// |
| /// ``` |
| /// { |
| /// "client_id": "46cc0ba6-f604-4fd9-aa2f-8a20beb24cd4", |
| /// "events": [{ "name": "testing", "params": { "time_ns": 345 } }], |
| /// "user_properties": { |
| /// "session_id": { "value": 1673466750423 }, |
| /// "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" }, |
| /// "tool": { "value": "flutter-tools" }, |
| /// "local_time": { "value": "2023-01-11 14:53:31.471816 -0500" } |
| /// } |
| /// } |
| /// ``` |
| /// https://developers.google.com/analytics/devguides/collection/protocol/ga4/sending-events?client_type=gtag |
| Map<String, Object?> generateRequestBody({ |
| required String clientId, |
| required DashEvent eventName, |
| required Map<String, Object?> eventData, |
| required UserProperty userProperty, |
| }) => |
| <String, Object?>{ |
| 'client_id': clientId, |
| 'events': <Map<String, Object?>>[ |
| <String, Object?>{ |
| 'name': eventName.label, |
| 'params': eventData, |
| } |
| ], |
| 'user_properties': userProperty.preparePayload() |
| }; |
| |
| /// This will use environment variables to get the user's |
| /// home directory where all the directory will be created that will |
| /// contain all of the analytics files |
| Directory getHomeDirectory(FileSystem fs) { |
| String? home; |
| Map<String, String> envVars = io.Platform.environment; |
| |
| if (io.Platform.isMacOS) { |
| home = envVars['HOME']; |
| } else if (io.Platform.isLinux) { |
| home = envVars['HOME']; |
| } else if (io.Platform.isWindows) { |
| home = envVars['AppData']; |
| } |
| |
| return fs.directory(home!); |
| } |
| |
| /// Returns `true` if user has opted out of legacy analytics in Dart or Flutter |
| /// |
| /// Checks legacy opt-out status for the Flutter |
| /// and Dart in the following locations |
| /// |
| /// Dart: `$HOME/.dart/dartdev.json` |
| /// |
| /// Flutter: `$HOME/.flutter` |
| bool legacyOptOut({ |
| required FileSystem fs, |
| required Directory home, |
| }) { |
| final File dartLegacyConfigFile = |
| fs.file(p.join(home.path, '.dart', 'dartdev.json')); |
| final File flutterLegacyConfigFile = fs.file(p.join(home.path, '.flutter')); |
| |
| // Example of what the file looks like for dart |
| // |
| // { |
| // "firstRun": false, |
| // "enabled": false, <-- THIS USER HAS OPTED OUT |
| // "disclosureShown": true, |
| // "clientId": "52710e60-7c70-4335-b3a4-9d922630f12a" |
| // } |
| if (dartLegacyConfigFile.existsSync()) { |
| try { |
| // Read in the json object into a Map and check for |
| // the enabled key being set to false; this means the user |
| // has opted out of analytics for dart |
| final Map<String, Object?> dartObj = |
| jsonDecode(dartLegacyConfigFile.readAsStringSync()); |
| if (dartObj.containsKey('enabled') && dartObj['enabled'] == false) { |
| return true; |
| } |
| } on FormatException { |
| // In the case of an error when parsing the json file, return true |
| // which will result in the user being opted out of unified_analytics |
| // |
| // A corrupted file could mean they opted out previously but for some |
| // reason, the file was written incorrectly |
| return true; |
| } on FileSystemException { |
| return true; |
| } |
| } |
| |
| // Example of what the file looks like for flutter |
| // |
| // { |
| // "firstRun": false, |
| // "clientId": "4c3a3d1e-e545-47e7-b4f8-10129f6ab169", |
| // "enabled": false <-- THIS USER HAS OPTED OUT |
| // } |
| if (flutterLegacyConfigFile.existsSync()) { |
| try { |
| // Same process as above for dart |
| final Map<String, Object?> flutterObj = |
| jsonDecode(dartLegacyConfigFile.readAsStringSync()); |
| if (flutterObj.containsKey('enabled') && flutterObj['enabled'] == false) { |
| return true; |
| } |
| } on FormatException { |
| // In the case of an error when parsing the json file, return true |
| // which will result in the user being opted out of unified_analytics |
| // |
| // A corrupted file could mean they opted out previously but for some |
| // reason, the file was written incorrectly |
| return true; |
| } on FileSystemException { |
| return true; |
| } |
| } |
| |
| return false; |
| } |
| |
| /// A UUID generator. |
| /// |
| /// This will generate unique IDs in the format: |
| /// |
| /// f47ac10b-58cc-4372-a567-0e02b2c3d479 |
| /// |
| /// The generated uuids are 128 bit numbers encoded in a specific string format. |
| /// For more information, see |
| /// [en.wikipedia.org/wiki/Universally_unique_identifier](http://en.wikipedia.org/wiki/Universally_unique_identifier). |
| /// |
| /// This class was taken from the previous `usage` package (https://github.com/dart-lang/usage/blob/master/lib/uuid/uuid.dart) |
| class Uuid { |
| final Random _random = Random(); |
| |
| /// Generate a version 4 (random) uuid. This is a uuid scheme that only uses |
| /// random numbers as the source of the generated uuid. |
| String generateV4() { |
| // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12. |
| int special = 8 + _random.nextInt(4); |
| |
| return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-' |
| '${_bitsDigits(16, 4)}-' |
| '4${_bitsDigits(12, 3)}-' |
| '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-' |
| '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}'; |
| } |
| |
| String _bitsDigits(int bitCount, int digitCount) => |
| _printDigits(_generateBits(bitCount), digitCount); |
| |
| int _generateBits(int bitCount) => _random.nextInt(1 << bitCount); |
| |
| String _printDigits(int value, int count) => |
| value.toRadixString(16).padLeft(count, '0'); |
| } |