blob: fa1f5102febcd4e05de10fe605816c7c61c2ccbb [file] [log] [blame]
// 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/local.dart';
import 'package:file/memory.dart';
import 'package:http/http.dart';
import 'package:path/path.dart' as p;
import 'config_handler.dart';
import 'constants.dart';
import 'enums.dart';
import 'ga_client.dart';
import 'initializer.dart';
import 'log_handler.dart';
import 'session.dart';
import 'user_property.dart';
import 'utils.dart';
abstract class Analytics {
/// The default factory constructor that will return an implementation
/// of the [Analytics] abstract class using the [LocalFileSystem]
factory Analytics({
required DashTool tool,
required String dartVersion,
String? flutterChannel,
String? flutterVersion,
}) {
// Create the instance of the file system so clients don't need
// resolve on their own
const FileSystem fs = LocalFileSystem();
// Resolve the OS using dart:io
final DevicePlatform platform;
if (io.Platform.operatingSystem == 'linux') {
platform = DevicePlatform.linux;
} else if (io.Platform.operatingSystem == 'macos') {
platform = DevicePlatform.macos;
} else {
platform = DevicePlatform.windows;
}
return AnalyticsImpl(
tool: tool.label,
homeDirectory: getHomeDirectory(fs),
measurementId: kGoogleAnalyticsMeasurementId,
apiSecret: kGoogleAnalyticsApiSecret,
flutterChannel: flutterChannel,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
platform: platform,
toolsMessage: kToolsMessage,
toolsMessageVersion: kToolsMessageVersion,
fs: fs,
);
}
/// Factory constructor to return the [AnalyticsImpl] class with a
/// [MemoryFileSystem] to use for testing
factory Analytics.test({
required String tool,
required Directory homeDirectory,
required String measurementId,
required String apiSecret,
String? flutterChannel,
String? flutterVersion,
required String dartVersion,
int toolsMessageVersion = kToolsMessageVersion,
String toolsMessage = kToolsMessage,
FileSystem? fs,
required DevicePlatform platform,
}) =>
TestAnalytics(
tool: tool,
homeDirectory: homeDirectory,
measurementId: measurementId,
apiSecret: apiSecret,
flutterChannel: flutterChannel,
toolsMessageVersion: toolsMessageVersion,
toolsMessage: toolsMessage,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
platform: platform,
fs: fs ??
MemoryFileSystem.test(
style: io.Platform.isWindows
? FileSystemStyle.windows
: FileSystemStyle.posix,
),
);
/// Returns a map object with all of the tools that have been parsed
/// out of the configuration file
Map<String, ToolInfo> get parsedTools;
/// Boolean that lets the client know if they should display the message
bool get shouldShowMessage;
/// Boolean indicating whether or not telemetry is enabled
bool get telemetryEnabled;
/// Returns the message that should be displayed to the users if
/// [shouldShowMessage] returns true
String get toolsMessage;
/// Returns a map representation of the [UserProperty] for the [Analytics] instance
///
/// This is what will get sent to Google Analytics with every request
Map<String, Map<String, Object?>> get userPropertyMap;
/// Call this method when the tool using this package is closed
///
/// Prevents the tool from hanging when if there are still requests
/// that need to be sent off
void close();
/// Query the persisted event data stored on the user's machine
///
/// Returns null if there are no persisted logs
LogFileStats? logFileStats();
/// API to send events to Google Analytics to track usage
Future<Response>? sendEvent({
required DashEvent eventName,
Map<String, Object?> eventData = const {},
});
/// Pass a boolean to either enable or disable telemetry and make
/// the necessary changes in the persisted configuration file
///
/// Setting the telemetry status will also send an event to GA
/// indicating the latest status of the telemetry from [reportingBool]
Future<void> setTelemetry(bool reportingBool);
}
class AnalyticsImpl implements Analytics {
final FileSystem fs;
late final ConfigHandler _configHandler;
late bool _showMessage;
late final GAClient _gaClient;
late final String _clientId;
late final UserProperty userProperty;
late final LogHandler _logHandler;
@override
final String toolsMessage;
AnalyticsImpl({
required String tool,
required Directory homeDirectory,
required String measurementId,
required String apiSecret,
String? flutterChannel,
String? flutterVersion,
required String dartVersion,
required DevicePlatform platform,
required this.toolsMessage,
required int toolsMessageVersion,
required this.fs,
}) {
// This initializer class will let the instance know
// if it was the first run; if it is, nothing will be sent
// on the first run
final Initializer initializer = Initializer(
fs: fs,
tool: tool,
homeDirectory: homeDirectory,
toolsMessageVersion: toolsMessageVersion,
toolsMessage: toolsMessage,
);
initializer.run();
_showMessage = initializer.firstRun;
// Create the config handler that will parse the config file
_configHandler = ConfigHandler(
fs: fs,
homeDirectory: homeDirectory,
initializer: initializer,
);
// Initialize the config handler class and check if the
// tool message and version have been updated from what
// is in the current file; if there is a new message version
// make the necessary updates
if (!_configHandler.parsedTools.containsKey(tool)) {
_configHandler.addTool(tool: tool);
_showMessage = true;
}
if (_configHandler.parsedTools[tool]!.versionNumber < toolsMessageVersion) {
_configHandler.incrementToolVersion(tool: tool);
_showMessage = true;
}
_clientId = fs
.file(p.join(
homeDirectory.path, kDartToolDirectoryName, kClientIdFileName))
.readAsStringSync();
// Create the instance of the GA Client which will create
// an [http.Client] to send requests
_gaClient = GAClient(
measurementId: measurementId,
apiSecret: apiSecret,
);
// Initialize the user property class that will be attached to
// each event that is sent to Google Analytics -- it will be responsible
// for getting the session id or rolling the session if the duration
// exceeds [kSessionDurationMinutes]
userProperty = UserProperty(
session: Session(homeDirectory: homeDirectory, fs: fs),
flutterChannel: flutterChannel,
host: platform.label,
flutterVersion: flutterVersion,
dartVersion: dartVersion,
tool: tool,
);
// Initialize the log handler to persist events that are being sent
_logHandler = LogHandler(fs: fs, homeDirectory: homeDirectory);
}
@override
Map<String, ToolInfo> get parsedTools => _configHandler.parsedTools;
@override
bool get shouldShowMessage => _showMessage;
@override
bool get telemetryEnabled => _configHandler.telemetryEnabled;
@override
Map<String, Map<String, Object?>> get userPropertyMap =>
userProperty.preparePayload();
@override
void close() => _gaClient.close();
@override
LogFileStats? logFileStats() => _logHandler.logFileStats();
@override
Future<Response>? sendEvent({
required DashEvent eventName,
Map<String, Object?> eventData = const {},
}) {
// Checking the [telemetryEnabled] boolean reflects what the
// config file reflects
//
// Checking the [_showMessage] boolean indicates if this the first
// time the tool is using analytics or if there has been an update
// the messaging found in constants.dart - in both cases, analytics
// will not be sent until the second time the tool is used
if (!telemetryEnabled || _showMessage) return null;
// Construct the body of the request
final Map<String, Object?> body = generateRequestBody(
clientId: _clientId,
eventName: eventName,
eventData: eventData,
userProperty: userProperty,
);
_logHandler.save(data: body);
// Pass to the google analytics client to send
return _gaClient.sendData(body);
}
@override
Future<void> setTelemetry(bool reportingBool) {
_configHandler.setTelemetry(reportingBool);
// Construct the body of the request to signal
// telemetry status toggling
//
// We use don't use the sendEvent method because it may
// be blocked by the [telemetryEnabled] getter
final Map<String, Object?> body = generateRequestBody(
clientId: _clientId,
eventName: DashEvent.analyticsCollectionEnabled,
eventData: {'status': reportingBool},
userProperty: userProperty,
);
_logHandler.save(data: body);
// Pass to the google analytics client to send
return _gaClient.sendData(body);
}
}
/// This class extends [AnalyticsImpl] and subs out any methods that
/// are not suitable for tests; the following have been altered from the
/// default implementation. All other methods are included
///
/// - `sendEvent(...)` has been altered to prevent data from being sent to GA
/// during testing
class TestAnalytics extends AnalyticsImpl {
TestAnalytics({
required super.tool,
required super.homeDirectory,
required super.measurementId,
required super.apiSecret,
super.flutterChannel,
super.flutterVersion,
required super.dartVersion,
required super.platform,
required super.toolsMessage,
required super.toolsMessageVersion,
required super.fs,
});
@override
Future<Response>? sendEvent({
required DashEvent eventName,
Map<String, Object?> eventData = const {},
}) {
if (!telemetryEnabled || _showMessage) return null;
// Calling the [generateRequestBody] method will ensure that the
// session file is getting updated without actually making any
// POST requests to Google Analytics
final Map<String, Object?> body = generateRequestBody(
clientId: _clientId,
eventName: eventName,
eventData: eventData,
userProperty: userProperty,
);
_logHandler.save(data: body);
return null;
}
}