Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ Future<AnalyticsController> get analyticsController async {

AnalyticsController? _analyticsController;

/// A synchronous check to see if analytics are enabled.
///
/// Returns `false` if analytics are disabled or not yet initialized.
bool get isAnalyticsEnabled =>
_analyticsController?.analyticsEnabled.value ?? false;

/// Whether the analytics controller has been initialized.
bool get isAnalyticsControllerInitialized => _analyticsController != null;

typedef AsyncAnalyticsCallback = FutureOr<void> Function();

class AnalyticsController {
Expand Down
18 changes: 17 additions & 1 deletion packages/devtools_app/lib/src/shared/server/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ import 'package:http/http.dart';
import 'package:logging/logging.dart';
import 'package:path/path.dart' as path;

import '../analytics/analytics_controller.dart';
import '../development_helpers.dart';
import '../globals.dart';
import '../primitives/query_parameters.dart';
import '../primitives/storage.dart';
import '../primitives/utils.dart';

Expand Down Expand Up @@ -73,7 +75,21 @@ Uri buildDevToolsServerRequestUri(String url) {
// [_debugDevToolsServerFlag] environment variable declaration was not set
// using `--dart-define`.
const baseUri = _debugDevToolsServerEnvironmentVariable;
return Uri.parse(path.join(baseUri, url));
final uri = Uri.parse(path.join(baseUri, url));

final queryParams = DevToolsQueryParams.load();
// Forward the parent IDE name and the client-side analytics opt-out status
// to the server, so they can be propagated to any spawned subprocesses.
// Fail-safe: default to suppressing analytics if the controller is not yet
// initialized.
final newParams = <String, String>{
...uri.queryParameters,
if (queryParams.ide != null) 'ide': queryParams.ide!,
if (!isAnalyticsControllerInitialized || !isAnalyticsEnabled)
'suppress_analytics': 'true',
};

return uri.replace(queryParameters: newParams);
}

/// Helper to catch any server request which could fail.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,28 @@ class DeeplinkManager {
/// APIs.
static const kOutputJsonField = 'json';

// TODO(https://github.com/flutter/devtools/issues/9702): Use the `DashTool`
// and `DashEnvVar` enums and `getEnvironment()` helper directly from
// `package:unified_analytics` once the pinned Flutter candidate SDK in this
// repository is bumped to a stable Dart SDK version >= 3.10.0 (resolving the
// dev SDK version solving conflict on CI).
/// Mappings from case-insensitive IDE query parameter values to their
/// corresponding DashTool canonical label strings used by `package:unified_analytics`.
///
/// Contains multiple spelling and format variations (with/without hyphens
/// or suffixes) passed by different IDE integrations to ensure O(1) lookup.
static const _ideToDashToolMap = <String, String>{
'vs-code': 'vscode-plugins',
'vscode': 'vscode-plugins',
'vscodeplugins': 'vscode-plugins',
'intellij-idea': 'intellij-plugins',
'intellij': 'intellij-plugins',
'intellijplugins': 'intellij-plugins',
'android-studio': 'android-studio-plugins',
'androidstudio': 'android-studio-plugins',
'androidstudioplugins': 'android-studio-plugins',
};

/// A regex to retrieve the file path from the stdout of iOS or Android
/// analyzers.
///
Expand All @@ -44,13 +66,31 @@ class DeeplinkManager {
Future<ProcessResult> runProcess(
String executable, {
required List<String> arguments,
String? ide,
bool suppressAnalytics = false,
}) {
final environment = <String, String>{
...Platform.environment,
'DASH__SUPPRESS_ANALYTICS': suppressAnalytics.toString(),
'DASH__TOOL': ide != null ? _mapIdeToDashToolLabel(ide) : 'devtools',
};

return Process.run(
executable,
arguments,
environment: environment,
);
}

String _mapIdeToDashToolLabel(String ide) {
final lowerIde = ide.toLowerCase();
final mappedTool = _ideToDashToolMap[lowerIde];
if (mappedTool != null) {
return mappedTool;
}
return 'devtools';
}

@visibleForTesting
String getFlutterBinary() {
// FLUTTER_ROOT can be set by Dart-Code VSCode extension or dart shell
Expand Down Expand Up @@ -81,9 +121,16 @@ class DeeplinkManager {
Future<String> _runFlutterCommand(
List<String> arguments, {
required RegExp outputMatcher,
String? ide,
bool suppressAnalytics = false,
}) async {
final flutterPath = getFlutterBinary();
final result = await runProcess(flutterPath, arguments: arguments);
final result = await runProcess(
flutterPath,
arguments: arguments,
ide: ide,
suppressAnalytics: suppressAnalytics,
);
if (result.exitCode != 0) {
throw _FlutterProcessError(
'Flutter command exit with non-zero error code ${result.exitCode}\n${result.stderr}',
Expand Down Expand Up @@ -126,10 +173,14 @@ class DeeplinkManager {

Future<Map<String, Object?>> getAndroidBuildVariants({
required String rootPath,
String? ide,
bool suppressAnalytics = false,
}) {
return _runFlutterCommand(
<String>['analyze', '--android', '--list-build-variants', rootPath],
outputMatcher: _androidBuildVariantJsonRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
).then<Map<String, Object?>>(
_handleJsonOutput,
onError: _handleRunFlutterError,
Expand All @@ -139,6 +190,8 @@ class DeeplinkManager {
Future<Map<String, Object?>> getAndroidAppLinkSettings({
required String rootPath,
required String buildVariant,
String? ide,
bool suppressAnalytics = false,
}) {
return _runFlutterCommand(
<String>[
Expand All @@ -149,6 +202,8 @@ class DeeplinkManager {
rootPath,
],
outputMatcher: _outputFilePathRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
).then<Map<String, Object?>>(
_handleReadJsonFile,
onError: _handleRunFlutterError,
Expand All @@ -157,10 +212,14 @@ class DeeplinkManager {

Future<Map<String, Object?>> getIosBuildOptions({
required String rootPath,
String? ide,
bool suppressAnalytics = false,
}) {
return _runFlutterCommand(
<String>['analyze', '--ios', '--list-build-options', rootPath],
outputMatcher: _iosBuildOptionsJsonRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
).then<Map<String, Object?>>(
_handleJsonOutput,
onError: _handleRunFlutterError,
Expand All @@ -171,6 +230,8 @@ class DeeplinkManager {
required String rootPath,
required String configuration,
required String target,
String? ide,
bool suppressAnalytics = false,
}) {
return _runFlutterCommand(
<String>[
Expand All @@ -182,6 +243,8 @@ class DeeplinkManager {
rootPath,
],
outputMatcher: _outputFilePathRegex,
ide: ide,
suppressAnalytics: suppressAnalytics,
).then<Map<String, Object?>>(
_handleReadJsonFile,
onError: _handleRunFlutterError,
Expand Down
22 changes: 19 additions & 3 deletions packages/devtools_shared/lib/src/server/handlers/_deeplink.dart
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,11 @@ extension _DeeplinkApiHandler on Never {
if (missingRequiredParams != null) return missingRequiredParams;

final rootPath = queryParams[DeeplinkApi.deeplinkRootPathPropertyName]!;
final result =
await deeplinkManager.getAndroidBuildVariants(rootPath: rootPath);
final result = await deeplinkManager.getAndroidBuildVariants(
rootPath: rootPath,
ide: queryParams.ide,
suppressAnalytics: queryParams.suppressAnalytics,
);
return _resultOutputOrError(api, result);
}

Expand All @@ -47,6 +50,8 @@ extension _DeeplinkApiHandler on Never {
final result = await deeplinkManager.getAndroidAppLinkSettings(
rootPath: rootPath,
buildVariant: buildVariant,
ide: queryParams.ide,
suppressAnalytics: queryParams.suppressAnalytics,
);
return _resultOutputOrError(api, result);
}
Expand All @@ -65,7 +70,11 @@ extension _DeeplinkApiHandler on Never {
if (missingRequiredParams != null) return missingRequiredParams;

final rootPath = queryParams[DeeplinkApi.deeplinkRootPathPropertyName]!;
final result = await deeplinkManager.getIosBuildOptions(rootPath: rootPath);
final result = await deeplinkManager.getIosBuildOptions(
rootPath: rootPath,
ide: queryParams.ide,
suppressAnalytics: queryParams.suppressAnalytics,
);
return _resultOutputOrError(api, result);
}

Expand All @@ -90,6 +99,8 @@ extension _DeeplinkApiHandler on Never {
rootPath: queryParams[DeeplinkApi.deeplinkRootPathPropertyName]!,
configuration: queryParams[DeeplinkApi.xcodeConfigurationPropertyName]!,
target: queryParams[DeeplinkApi.xcodeTargetPropertyName]!,
ide: queryParams.ide,
suppressAnalytics: queryParams.suppressAnalytics,
);
return _resultOutputOrError(api, result);
}
Expand All @@ -107,3 +118,8 @@ extension _DeeplinkApiHandler on Never {
);
}
}

extension on Map<String, String> {
String? get ide => this['ide'];
bool get suppressAnalytics => this['suppress_analytics'] == 'true';
}
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,43 @@ Running Gradle task 'printBuildVariants'... 10.4s
);
});

test('getBuildVariants propagates parent IDE and analytics opt-out status',
() async {
const projectRoot = '/abc';
manager.expectedCommands.add(
TestCommand(
executable: manager.mockedFlutterBinary,
arguments: <String>[
'analyze',
'--android',
'--list-build-variants',
projectRoot,
],
ide: 'VS-Code',
suppressAnalytics: true,
result: ProcessResult(
0,
0,
r'''
Running Gradle task 'printBuildVariants'... 10.4s
["debug"]
''',
'',
),
),
);
final response = await manager.getAndroidBuildVariants(
rootPath: projectRoot,
ide: 'VS-Code',
suppressAnalytics: true,
);
expect(response[DeeplinkManager.kErrorField], isNull);
expect(
response[DeeplinkManager.kOutputJsonField],
'["debug"]',
);
});

test(
'getBuildVariants return internal server error if command failed',
() async {
Expand Down Expand Up @@ -217,15 +254,19 @@ class StubbedDeeplinkManager extends DeeplinkManager {
Future<ProcessResult> runProcess(
String executable, {
required List<String> arguments,
String? ide,
bool suppressAnalytics = false,
}) async {
if (expectedCommands.isNotEmpty) {
final expectedCommand = expectedCommands.removeAt(0);
expect(expectedCommand.executable, executable);
expect(executable, expectedCommand.executable);
expect(
const ListEquality<String>()
.equals(expectedCommand.arguments, arguments),
.equals(arguments, expectedCommand.arguments),
isTrue,
);
expect(ide, expectedCommand.ide);
expect(suppressAnalytics, expectedCommand.suppressAnalytics);
return expectedCommand.result;
}
throw 'Received unexpected command: $executable ${arguments.join(' ')}';
Expand All @@ -236,10 +277,14 @@ class TestCommand {
const TestCommand({
required this.executable,
required this.arguments,
this.ide,
this.suppressAnalytics = false,
required this.result,
});
final String executable;
final List<String> arguments;
final String? ide;
final bool suppressAnalytics;
final ProcessResult result;

@override
Expand Down
24 changes: 16 additions & 8 deletions packages/devtools_shared/test/fakes.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,42 +11,50 @@ class FakeDeeplinkManager extends DeeplinkManager {
String? receivedBuildVariant;
String? receivedConfiguration;
String? receivedTarget;
late Map<String, String> responseForGetAndroidBuildVariants;
late Map<String, String> responseForGetAndroidAppLinkSettings;
late Map<String, String> responseForGetIosBuildOptions;
late Map<String, String> responseForGetIosUniversalLinkSettings;
late Map<String, Object?> responseForGetAndroidBuildVariants;
late Map<String, Object?> responseForGetAndroidAppLinkSettings;
late Map<String, Object?> responseForGetIosBuildOptions;
late Map<String, Object?> responseForGetIosUniversalLinkSettings;

@override
Future<Map<String, String>> getAndroidBuildVariants({
Future<Map<String, Object?>> getAndroidBuildVariants({
required String rootPath,
String? ide,
bool suppressAnalytics = false,
}) async {
receivedPath = rootPath;
return responseForGetAndroidBuildVariants;
}

@override
Future<Map<String, String>> getAndroidAppLinkSettings({
Future<Map<String, Object?>> getAndroidAppLinkSettings({
required String rootPath,
required String buildVariant,
String? ide,
bool suppressAnalytics = false,
}) async {
receivedPath = rootPath;
receivedBuildVariant = buildVariant;
return responseForGetAndroidAppLinkSettings;
}

@override
Future<Map<String, String>> getIosBuildOptions({
Future<Map<String, Object?>> getIosBuildOptions({
required String rootPath,
String? ide,
bool suppressAnalytics = false,
}) async {
receivedPath = rootPath;
return responseForGetIosBuildOptions;
}

@override
Future<Map<String, String>> getIosUniversalLinkSettings({
Future<Map<String, Object?>> getIosUniversalLinkSettings({
required String rootPath,
required String configuration,
required String target,
String? ide,
bool suppressAnalytics = false,
}) async {
receivedPath = rootPath;
receivedConfiguration = configuration;
Expand Down
Loading