Skip to content

Honor legacy opt out status #80

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 13 commits into from
Apr 17, 2023
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion pkgs/unified_analytics/lib/src/initializer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,16 @@ class Initializer {
required int toolsMessageVersion,
}) {
configFile.createSync(recursive: true);
configFile.writeAsStringSync(kConfigString);

// 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)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless I'm reading this package's code wrong, it looks like this will be platform.environment['UserProfile'] on windows (https://github.com/dart-lang/tools/blob/main/pkgs/unified_analytics/lib/src/utils.dart#L78), when the tool will be writing to platform.environment['AppData'] on Windows https://github.com/dart-lang/usage/blob/master/lib/src/usage_impl_io.dart#L89

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh that's a very good catch. In the PDD, it says all files will be written to the user's home directory, am I correct to assume the home directory on windows is stored in UserProfile?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

my understanding is that "home directory" is an imprecise term, and especially on Windows there is not one "correct" answer for where a user's home directory is across different Windows versions and also application usages. In other words:

  1. for the purposes of this PR, I think we need to check for legacy opt out from where package:usage is writing it, which is the AppData env var if present (not sure if it's ever not present, or what package:usage does in that case).
  2. for the purposes of fulfilling the PDD, I'm not sure what we should do.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The home directory as the value of the UserProfile environment variable seems correct to me and fulfills the PDD. The most common definitions of "home directory" under Windows align with this.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably should adjust the PDD to allow writing the files in a more Windows-friendly location like AppData.

Copy link
Contributor Author

@eliasyishak eliasyishak Apr 17, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we would be okay if we changed the env variable for home on windows to point to APPDATA then? That way we can continue what was done in the past and address @christopherfujino 's item 1 above?

configFile.writeAsStringSync(
kConfigString.replaceAll('reporting=1', 'reporting=0'));
} else {
configFile.writeAsStringSync(kConfigString);
}
}

/// Creates that log file that will store the record formatted
Expand Down
64 changes: 64 additions & 0 deletions pkgs/unified_analytics/lib/src/utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// 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';
Expand Down Expand Up @@ -81,6 +83,68 @@ Directory getHomeDirectory(FileSystem fs) {
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"
// }
try {
if (dartLegacyConfigFile.existsSync()) {
// 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;
}
}
} catch (e) {
// Continue if the file was something we couldn't parse
}

// Example of what the file looks like for flutter
//
// {
// "firstRun": false,
// "clientId": "4c3a3d1e-e545-47e7-b4f8-10129f6ab169",
// "enabled": false <-- THIS USER HAS OPTED OUT
// }
try {
if (flutterLegacyConfigFile.existsSync()) {
// Same process as above for dart
final Map<String, Object?> flutterObj =
jsonDecode(dartLegacyConfigFile.readAsStringSync());
if (flutterObj.containsKey('enabled') && flutterObj['enabled'] == false) {
return true;
}
}
} catch (e) {
// Continue if the file was something we couldn't parse
}

return false;
}

/// A UUID generator.
///
/// This will generate unique IDs in the format:
Expand Down
178 changes: 178 additions & 0 deletions pkgs/unified_analytics/test/legacy_analytics_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
// 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:io' as io;

import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:test/test.dart';

import 'package:unified_analytics/unified_analytics.dart';

void main() {
late FileSystem fs;
late Directory home;
late Analytics analytics;

const String homeDirName = 'home';
const DashTool initialTool = DashTool.flutterTool;
const String measurementId = 'measurementId';
const String apiSecret = 'apiSecret';
const int toolsMessageVersion = 1;
const String toolsMessage = 'toolsMessage';
const String flutterChannel = 'flutterChannel';
const String flutterVersion = 'flutterVersion';
const String dartVersion = 'dartVersion';
const DevicePlatform platform = DevicePlatform.macos;

setUp(() {
// Setup the filesystem with the home directory
final FileSystemStyle fsStyle =
io.Platform.isWindows ? FileSystemStyle.windows : FileSystemStyle.posix;
fs = MemoryFileSystem.test(style: fsStyle);
home = fs.directory(homeDirName);
});

test('Honor legacy dart analytics opt out', () {
// Create the file for the dart legacy opt out
final File dartLegacyConfigFile =
home.childDirectory('.dart').childFile('dartdev.json');
dartLegacyConfigFile.createSync(recursive: true);
dartLegacyConfigFile.writeAsStringSync('''
{
"firstRun": false,
"enabled": false,
"disclosureShown": true,
"clientId": "52710e60-7c70-4335-b3a4-9d922630f12a"
}
''');

// The main analytics instance, other instances can be spawned within tests
// to test how to instances running together work
//
// This instance should have the same parameters as the one above for
// [initializationAnalytics]
analytics = Analytics.test(
tool: initialTool,
homeDirectory: home,
measurementId: measurementId,
apiSecret: apiSecret,
flutterChannel: flutterChannel,
toolsMessageVersion: toolsMessageVersion,
toolsMessage: toolsMessage,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
fs: fs,
platform: platform,
);

expect(analytics.telemetryEnabled, false);
});

test('Telemetry enabled if legacy dart analytics is enabled', () {
// Create the file for the dart legacy opt out
final File dartLegacyConfigFile =
home.childDirectory('.dart').childFile('dartdev.json');
dartLegacyConfigFile.createSync(recursive: true);
dartLegacyConfigFile.writeAsStringSync('''
{
"firstRun": false,
"enabled": true,
"disclosureShown": true,
"clientId": "52710e60-7c70-4335-b3a4-9d922630f12a"
}
''');

// The main analytics instance, other instances can be spawned within tests
// to test how to instances running together work
//
// This instance should have the same parameters as the one above for
// [initializationAnalytics]
analytics = Analytics.test(
tool: initialTool,
homeDirectory: home,
measurementId: measurementId,
apiSecret: apiSecret,
flutterChannel: flutterChannel,
toolsMessageVersion: toolsMessageVersion,
toolsMessage: toolsMessage,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
fs: fs,
platform: platform,
);

expect(analytics.telemetryEnabled, true);
});

test('Honor legacy flutter analytics opt out', () {
// Create the file for the dart legacy opt out
final File flutterLegacyConfigFile =
home.childDirectory('.dart').childFile('dartdev.json');
flutterLegacyConfigFile.createSync(recursive: true);
flutterLegacyConfigFile.writeAsStringSync('''
{
"firstRun": false,
"clientId": "4c3a3d1e-e545-47e7-b4f8-10129f6ab169",
"enabled": false
}
''');

// The main analytics instance, other instances can be spawned within tests
// to test how to instances running together work
//
// This instance should have the same parameters as the one above for
// [initializationAnalytics]
analytics = Analytics.test(
tool: initialTool,
homeDirectory: home,
measurementId: measurementId,
apiSecret: apiSecret,
flutterChannel: flutterChannel,
toolsMessageVersion: toolsMessageVersion,
toolsMessage: toolsMessage,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
fs: fs,
platform: platform,
);

expect(analytics.telemetryEnabled, false);
});

test('Telemetry enabled if legacy flutter analytics is enabled', () {
// Create the file for the dart legacy opt out
final File flutterLegacyConfigFile =
home.childDirectory('.dart').childFile('dartdev.json');
flutterLegacyConfigFile.createSync(recursive: true);
flutterLegacyConfigFile.writeAsStringSync('''
{
"firstRun": false,
"clientId": "4c3a3d1e-e545-47e7-b4f8-10129f6ab169",
"enabled": true
}
''');

// The main analytics instance, other instances can be spawned within tests
// to test how to instances running together work
//
// This instance should have the same parameters as the one above for
// [initializationAnalytics]
analytics = Analytics.test(
tool: initialTool,
homeDirectory: home,
measurementId: measurementId,
apiSecret: apiSecret,
flutterChannel: flutterChannel,
toolsMessageVersion: toolsMessageVersion,
toolsMessage: toolsMessage,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
fs: fs,
platform: platform,
);

expect(analytics.telemetryEnabled, true);
});
}