mirror of
https://github.com/flutter/flutter.git
synced 2026-02-20 02:29:02 +08:00
<!-- Thanks for filing a pull request! Reviewers are typically assigned within a week of filing a request. To learn more about code review, see our documentation on Tree Hygiene: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md --> Converted more null checks to null aware elements. The analyzer had many false negatives and the regex became pretty wild to find many of them 😂 I've found more in the engine folder but I'll apply the changes in a separate PR Part of #172188 *If you had to change anything in the [flutter/tests] repo, include a link to the migration guide as per the [breaking change policy].* ## Pre-launch Checklist - [x] I read the [Contributor Guide] and followed the process outlined there for submitting PRs. - [x] I read the [Tree Hygiene] wiki page, which explains my responsibilities. - [x] I read and followed the [Flutter Style Guide], including [Features we expect every widget to implement]. - [x] I signed the [CLA]. - [x] I listed at least one issue that this PR fixes in the description above. - [x] I updated/added relevant documentation (doc comments with `///`). - [x] I added new tests to check the change I am making, or this PR is [test-exempt]. - [x] I followed the [breaking change policy] and added [Data Driven Fixes] where supported. - [x] All existing and new tests are passing. If you need help, consider asking for advice on the #hackers-new channel on [Discord]. **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. <!-- Links --> [Contributor Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#overview [Tree Hygiene]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md [test-exempt]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#tests [Flutter Style Guide]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md [Features we expect every widget to implement]: https://github.com/flutter/flutter/blob/main/docs/contributing/Style-guide-for-Flutter-repo.md#features-we-expect-every-widget-to-implement [CLA]: https://cla.developers.google.com/ [flutter/tests]: https://github.com/flutter/tests [breaking change policy]: https://github.com/flutter/flutter/blob/main/docs/contributing/Tree-hygiene.md#handling-breaking-changes [Discord]: https://github.com/flutter/flutter/blob/main/docs/contributing/Chat.md [Data Driven Fixes]: https://github.com/flutter/flutter/blob/main/docs/contributing/Data-driven-Fixes.md
561 lines
16 KiB
Dart
561 lines
16 KiB
Dart
// Copyright 2014 The Flutter Authors. All rights reserved.
|
|
// Use of this source code is governed by a BSD-style license that can be
|
|
// found in the LICENSE file.
|
|
|
|
part of 'reporting.dart';
|
|
|
|
const _kFlutterUA = 'UA-67589403-6';
|
|
|
|
abstract class Usage {
|
|
/// Create a new Usage instance; [versionOverride], [configDirOverride], and
|
|
/// [logFile] are used for testing.
|
|
factory Usage({
|
|
String settingsName = 'flutter',
|
|
String? versionOverride,
|
|
String? configDirOverride,
|
|
String? logFile,
|
|
AnalyticsFactory? analyticsIOFactory,
|
|
FirstRunMessenger? firstRunMessenger,
|
|
required bool runningOnBot,
|
|
}) => _DefaultUsage.initialize(
|
|
settingsName: settingsName,
|
|
versionOverride: versionOverride,
|
|
configDirOverride: configDirOverride,
|
|
logFile: logFile,
|
|
analyticsIOFactory: analyticsIOFactory,
|
|
runningOnBot: runningOnBot,
|
|
firstRunMessenger: firstRunMessenger,
|
|
);
|
|
|
|
/// Uses the global [Usage] instance to send a 'command' to analytics.
|
|
static void command(String command, {CustomDimensions? parameters}) =>
|
|
globals.flutterUsage.sendCommand(command, parameters: parameters);
|
|
|
|
/// Whether analytics reporting should be suppressed.
|
|
bool get suppressAnalytics;
|
|
|
|
/// Suppress analytics for this session.
|
|
set suppressAnalytics(bool value);
|
|
|
|
/// Whether analytics reporting is enabled.
|
|
bool get enabled;
|
|
|
|
/// Enable or disable reporting analytics.
|
|
set enabled(bool value);
|
|
|
|
/// A stable randomly generated UUID used to deduplicate multiple identical
|
|
/// reports coming from the same computer.
|
|
String get clientId;
|
|
|
|
/// Sends a 'command' to the underlying analytics implementation.
|
|
///
|
|
/// Using [command] above is preferred to ensure that the parameter
|
|
/// keys are well-defined in [CustomDimensions] above.
|
|
void sendCommand(String command, {CustomDimensions? parameters});
|
|
|
|
/// Sends an 'event' to the underlying analytics implementation.
|
|
///
|
|
/// This method should not be used directly, instead see the
|
|
/// event types defined in this directory in `events.dart`.
|
|
@visibleForOverriding
|
|
@visibleForTesting
|
|
void sendEvent(
|
|
String category,
|
|
String parameter, {
|
|
String? label,
|
|
int? value,
|
|
CustomDimensions? parameters,
|
|
});
|
|
|
|
/// Sends timing information to the underlying analytics implementation.
|
|
void sendTiming(String category, String variableName, Duration duration, {String? label});
|
|
|
|
/// Sends an exception to the underlying analytics implementation.
|
|
void sendException(dynamic exception);
|
|
|
|
/// Fires whenever analytics data is sent over the network.
|
|
@visibleForTesting
|
|
Stream<Map<String, dynamic>> get onSend;
|
|
|
|
/// Returns when the last analytics event has been sent, or after a fixed
|
|
/// (short) delay, whichever is less.
|
|
Future<void> ensureAnalyticsSent();
|
|
|
|
/// Prints a welcome message that informs the tool user about the collection
|
|
/// of anonymous usage information.
|
|
void printWelcome();
|
|
}
|
|
|
|
typedef AnalyticsFactory =
|
|
Analytics Function(
|
|
String trackingId,
|
|
String applicationName,
|
|
String applicationVersion, {
|
|
String analyticsUrl,
|
|
Directory? documentDirectory,
|
|
});
|
|
|
|
Analytics _defaultAnalyticsIOFactory(
|
|
String trackingId,
|
|
String applicationName,
|
|
String applicationVersion, {
|
|
String? analyticsUrl,
|
|
Directory? documentDirectory,
|
|
}) {
|
|
return AnalyticsIO(
|
|
trackingId,
|
|
applicationName,
|
|
applicationVersion,
|
|
analyticsUrl: analyticsUrl,
|
|
documentDirectory: documentDirectory,
|
|
);
|
|
}
|
|
|
|
class _DefaultUsage implements Usage {
|
|
_DefaultUsage._({
|
|
required bool suppressAnalytics,
|
|
required Analytics analytics,
|
|
required this.firstRunMessenger,
|
|
required SystemClock clock,
|
|
}) : _suppressAnalytics = suppressAnalytics,
|
|
_analytics = analytics,
|
|
_clock = clock;
|
|
|
|
static _DefaultUsage initialize({
|
|
String settingsName = 'flutter',
|
|
String? versionOverride,
|
|
String? configDirOverride,
|
|
String? logFile,
|
|
AnalyticsFactory? analyticsIOFactory,
|
|
required FirstRunMessenger? firstRunMessenger,
|
|
required bool runningOnBot,
|
|
}) {
|
|
final FlutterVersion flutterVersion = globals.flutterVersion;
|
|
final String version =
|
|
versionOverride ?? flutterVersion.getVersionString(redactUnknownBranches: true);
|
|
final suppressEnvFlag = globals.platform.environment['FLUTTER_SUPPRESS_ANALYTICS'] == 'true';
|
|
final String? logFilePath =
|
|
logFile ?? globals.platform.environment['FLUTTER_ANALYTICS_LOG_FILE'];
|
|
final bool usingLogFile = logFilePath != null && logFilePath.isNotEmpty;
|
|
|
|
final AnalyticsFactory analyticsFactory = analyticsIOFactory ?? _defaultAnalyticsIOFactory;
|
|
var suppressAnalytics = false;
|
|
var skipAnalyticsSessionSetup = false;
|
|
Analytics? setupAnalytics;
|
|
if ( // To support testing, only allow other signals to suppress analytics
|
|
// when analytics are not being shunted to a file.
|
|
!usingLogFile &&
|
|
( // Ignore local user branches.
|
|
version.startsWith('[user-branch]') ||
|
|
// Many CI systems don't do a full git checkout.
|
|
version.endsWith('/unknown') ||
|
|
// Ignore bots.
|
|
runningOnBot ||
|
|
// Ignore when suppressed by FLUTTER_SUPPRESS_ANALYTICS.
|
|
suppressEnvFlag)) {
|
|
// If we think we're running on a CI system, suppress sending analytics.
|
|
suppressAnalytics = true;
|
|
setupAnalytics = AnalyticsMock();
|
|
skipAnalyticsSessionSetup = true;
|
|
}
|
|
if (usingLogFile) {
|
|
setupAnalytics ??= LogToFileAnalytics(logFilePath);
|
|
} else {
|
|
try {
|
|
ErrorHandlingFileSystem.noExitOnFailure(() {
|
|
setupAnalytics = analyticsFactory(
|
|
_kFlutterUA,
|
|
settingsName,
|
|
version,
|
|
documentDirectory: configDirOverride != null
|
|
? globals.fs.directory(configDirOverride)
|
|
: null,
|
|
);
|
|
});
|
|
} on Exception catch (e) {
|
|
globals.printTrace('Failed to initialize analytics reporting: $e');
|
|
suppressAnalytics = true;
|
|
setupAnalytics ??= AnalyticsMock();
|
|
skipAnalyticsSessionSetup = true;
|
|
}
|
|
}
|
|
|
|
final Analytics analytics = setupAnalytics!;
|
|
if (!skipAnalyticsSessionSetup) {
|
|
// Report a more detailed OS version string than package:usage does by default.
|
|
analytics.setSessionValue(CustomDimensionsEnum.sessionHostOsDetails.cdKey, globals.os.name);
|
|
// Send the branch name as the "channel".
|
|
analytics.setSessionValue(
|
|
CustomDimensionsEnum.sessionChannelName.cdKey,
|
|
flutterVersion.getBranchName(redactUnknownBranches: true),
|
|
);
|
|
// For each flutter experimental feature, record a session value in a comma
|
|
// separated list.
|
|
final String enabledFeatures = featureFlags.allFeatures
|
|
.where((Feature feature) {
|
|
final String? configSetting = feature.configSetting;
|
|
return configSetting != null && globals.config.getValue(configSetting) == true;
|
|
})
|
|
.map((Feature feature) => feature.configSetting)
|
|
.join(',');
|
|
analytics.setSessionValue(CustomDimensionsEnum.enabledFlutterFeatures.cdKey, enabledFeatures);
|
|
|
|
// Record the host as the application installer ID - the context that flutter_tools is running in.
|
|
if (globals.platform.environment.containsKey('FLUTTER_HOST')) {
|
|
analytics.setSessionValue('aiid', globals.platform.environment['FLUTTER_HOST']);
|
|
}
|
|
analytics.analyticsOpt = AnalyticsOpt.optOut;
|
|
}
|
|
|
|
return _DefaultUsage._(
|
|
suppressAnalytics: suppressAnalytics,
|
|
analytics: analytics,
|
|
firstRunMessenger: firstRunMessenger,
|
|
clock: globals.systemClock,
|
|
);
|
|
}
|
|
|
|
final Analytics _analytics;
|
|
final FirstRunMessenger? firstRunMessenger;
|
|
|
|
var _printedWelcome = false;
|
|
var _suppressAnalytics = false;
|
|
final SystemClock _clock;
|
|
|
|
@override
|
|
bool get suppressAnalytics => _suppressAnalytics || _analytics.firstRun;
|
|
|
|
@override
|
|
set suppressAnalytics(bool value) {
|
|
_suppressAnalytics = value;
|
|
}
|
|
|
|
@override
|
|
bool get enabled => _analytics.enabled;
|
|
|
|
@override
|
|
set enabled(bool value) {
|
|
_analytics.enabled = value;
|
|
}
|
|
|
|
@override
|
|
String get clientId => _analytics.clientId;
|
|
|
|
@override
|
|
void sendCommand(String command, {CustomDimensions? parameters}) {
|
|
if (suppressAnalytics) {
|
|
return;
|
|
}
|
|
|
|
_analytics.sendScreenView(
|
|
command,
|
|
parameters: CustomDimensions(
|
|
localTime: formatDateTime(_clock.now()),
|
|
).merge(parameters).toMap(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void sendEvent(
|
|
String category,
|
|
String parameter, {
|
|
String? label,
|
|
int? value,
|
|
CustomDimensions? parameters,
|
|
}) {
|
|
if (suppressAnalytics) {
|
|
return;
|
|
}
|
|
|
|
_analytics.sendEvent(
|
|
category,
|
|
parameter,
|
|
label: label,
|
|
value: value,
|
|
parameters: CustomDimensions(
|
|
localTime: formatDateTime(_clock.now()),
|
|
).merge(parameters).toMap(),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void sendTiming(String category, String variableName, Duration duration, {String? label}) {
|
|
if (suppressAnalytics) {
|
|
return;
|
|
}
|
|
_analytics.sendTiming(variableName, duration.inMilliseconds, category: category, label: label);
|
|
}
|
|
|
|
@override
|
|
void sendException(dynamic exception) {
|
|
if (suppressAnalytics) {
|
|
return;
|
|
}
|
|
_analytics.sendException(exception.runtimeType.toString());
|
|
}
|
|
|
|
@override
|
|
Stream<Map<String, dynamic>> get onSend => _analytics.onSend;
|
|
|
|
@override
|
|
Future<void> ensureAnalyticsSent() async {
|
|
// TODO(devoncarew): This may delay tool exit and could cause some analytics
|
|
// events to not be reported. Perhaps we could send the analytics pings
|
|
// out-of-process from flutter_tools?
|
|
await _analytics.waitForLastPing(timeout: const Duration(milliseconds: 250));
|
|
}
|
|
|
|
@override
|
|
void printWelcome() {
|
|
// Only print once per run.
|
|
if (_printedWelcome) {
|
|
return;
|
|
}
|
|
// Display the welcome message if this is the first run of the tool or if
|
|
// the license terms have changed since it was last displayed.
|
|
final FirstRunMessenger? messenger = firstRunMessenger;
|
|
if (messenger != null && messenger.shouldDisplayLicenseTerms()) {
|
|
globals.printStatus('');
|
|
globals.printStatus(messenger.licenseTerms, emphasis: true);
|
|
_printedWelcome = true;
|
|
messenger.confirmLicenseTermsDisplayed();
|
|
}
|
|
}
|
|
}
|
|
|
|
// An Analytics mock that logs to file. Unimplemented methods goes to stdout.
|
|
// But stdout can't be used for testing since wrapper scripts like
|
|
// xcode_backend.sh etc manipulates them.
|
|
class LogToFileAnalytics extends AnalyticsMock {
|
|
LogToFileAnalytics(String logFilePath)
|
|
: logFile = globals.fs.file(logFilePath)..createSync(recursive: true),
|
|
super(true);
|
|
|
|
final File logFile;
|
|
final _sessionValues = <String, String>{};
|
|
|
|
final _sendController = StreamController<Map<String, dynamic>>.broadcast(sync: true);
|
|
|
|
@override
|
|
Stream<Map<String, dynamic>> get onSend => _sendController.stream;
|
|
|
|
@override
|
|
Future<void> sendScreenView(String viewName, {Map<String, String>? parameters}) {
|
|
if (!enabled) {
|
|
return Future<void>.value();
|
|
}
|
|
parameters ??= <String, String>{};
|
|
parameters['viewName'] = viewName;
|
|
parameters.addAll(_sessionValues);
|
|
_sendController.add(parameters);
|
|
logFile.writeAsStringSync('screenView $parameters\n', mode: FileMode.append);
|
|
return Future<void>.value();
|
|
}
|
|
|
|
@override
|
|
Future<void> sendEvent(
|
|
String category,
|
|
String action, {
|
|
String? label,
|
|
int? value,
|
|
Map<String, String>? parameters,
|
|
}) {
|
|
if (!enabled) {
|
|
return Future<void>.value();
|
|
}
|
|
parameters ??= <String, String>{};
|
|
parameters['category'] = category;
|
|
parameters['action'] = action;
|
|
_sendController.add(parameters);
|
|
logFile.writeAsStringSync('event $parameters\n', mode: FileMode.append);
|
|
return Future<void>.value();
|
|
}
|
|
|
|
@override
|
|
Future<void> sendTiming(String variableName, int time, {String? category, String? label}) {
|
|
if (!enabled) {
|
|
return Future<void>.value();
|
|
}
|
|
final parameters = <String, String>{
|
|
'variableName': variableName,
|
|
'time': '$time',
|
|
'category': ?category,
|
|
'label': ?label,
|
|
};
|
|
_sendController.add(parameters);
|
|
logFile.writeAsStringSync('timing $parameters\n', mode: FileMode.append);
|
|
return Future<void>.value();
|
|
}
|
|
|
|
@override
|
|
void setSessionValue(String param, dynamic value) {
|
|
_sessionValues[param] = value.toString();
|
|
}
|
|
}
|
|
|
|
/// Create a testing Usage instance.
|
|
///
|
|
/// All sent events, exceptions, timings, and pages are
|
|
/// buffered on the object and can be inspected later.
|
|
@visibleForTesting
|
|
class TestUsage implements Usage {
|
|
final commands = <TestUsageCommand>[];
|
|
final events = <TestUsageEvent>[];
|
|
final exceptions = <dynamic>[];
|
|
final timings = <TestTimingEvent>[];
|
|
var ensureAnalyticsSentCalls = 0;
|
|
var _printedWelcome = false;
|
|
|
|
@override
|
|
var enabled = true;
|
|
|
|
@override
|
|
var suppressAnalytics = false;
|
|
|
|
@override
|
|
String get clientId => 'test-client';
|
|
|
|
/// Confirms if the [printWelcome] method was invoked.
|
|
bool get printedWelcome => _printedWelcome;
|
|
|
|
@override
|
|
Future<void> ensureAnalyticsSent() async {
|
|
ensureAnalyticsSentCalls++;
|
|
}
|
|
|
|
@override
|
|
Stream<Map<String, dynamic>> get onSend => throw UnimplementedError();
|
|
|
|
@override
|
|
void printWelcome() {
|
|
_printedWelcome = true;
|
|
}
|
|
|
|
@override
|
|
void sendCommand(String command, {CustomDimensions? parameters}) {
|
|
commands.add(TestUsageCommand(command, parameters: parameters));
|
|
}
|
|
|
|
@override
|
|
void sendEvent(
|
|
String category,
|
|
String parameter, {
|
|
String? label,
|
|
int? value,
|
|
CustomDimensions? parameters,
|
|
}) {
|
|
events.add(
|
|
TestUsageEvent(category, parameter, label: label, value: value, parameters: parameters),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void sendException(dynamic exception) {
|
|
exceptions.add(exception);
|
|
}
|
|
|
|
@override
|
|
void sendTiming(String category, String variableName, Duration duration, {String? label}) {
|
|
timings.add(TestTimingEvent(category, variableName, duration, label: label));
|
|
}
|
|
}
|
|
|
|
@visibleForTesting
|
|
@immutable
|
|
class TestUsageCommand {
|
|
const TestUsageCommand(this.command, {this.parameters});
|
|
|
|
final String command;
|
|
final CustomDimensions? parameters;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return other is TestUsageCommand && other.command == command && other.parameters == parameters;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(command, parameters);
|
|
|
|
@override
|
|
String toString() => 'TestUsageCommand($command, parameters:$parameters)';
|
|
}
|
|
|
|
@visibleForTesting
|
|
@immutable
|
|
class TestUsageEvent {
|
|
const TestUsageEvent(this.category, this.parameter, {this.label, this.value, this.parameters});
|
|
|
|
final String category;
|
|
final String parameter;
|
|
final String? label;
|
|
final int? value;
|
|
final CustomDimensions? parameters;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return other is TestUsageEvent &&
|
|
other.category == category &&
|
|
other.parameter == parameter &&
|
|
other.label == label &&
|
|
other.value == value &&
|
|
other.parameters == parameters;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(category, parameter, label, value, parameters);
|
|
|
|
@override
|
|
String toString() =>
|
|
'TestUsageEvent($category, $parameter, label:$label, value:$value, parameters:$parameters)';
|
|
}
|
|
|
|
@visibleForTesting
|
|
@immutable
|
|
class TestTimingEvent {
|
|
const TestTimingEvent(this.category, this.variableName, this.duration, {this.label});
|
|
|
|
final String category;
|
|
final String variableName;
|
|
final Duration duration;
|
|
final String? label;
|
|
|
|
@override
|
|
bool operator ==(Object other) {
|
|
return other is TestTimingEvent &&
|
|
other.category == category &&
|
|
other.variableName == variableName &&
|
|
other.duration == duration &&
|
|
other.label == label;
|
|
}
|
|
|
|
@override
|
|
int get hashCode => Object.hash(category, variableName, duration, label);
|
|
|
|
@override
|
|
String toString() => 'TestTimingEvent($category, $variableName, $duration, label:$label)';
|
|
}
|
|
|
|
bool _mapsEqual(Map<String, String>? a, Map<String, String>? b) {
|
|
if (a == b) {
|
|
return true;
|
|
}
|
|
if (a == null || b == null) {
|
|
return false;
|
|
}
|
|
if (a.length != b.length) {
|
|
return false;
|
|
}
|
|
|
|
for (final String k in a.keys) {
|
|
final String? bValue = b[k];
|
|
if (bValue == null && !b.containsKey(k)) {
|
|
return false;
|
|
}
|
|
if (bValue != a[k]) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|