blob: 69dd392b244b0f91cb33ac032b7d86bd82ff7e05 [file] [log] [blame]
// Copyright (c) 2014, 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:async';
import 'dart:convert' show JsonEncoder, jsonDecode;
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:path/path.dart' as path;
import 'usage_impl.dart';
/// An interface to a Google Analytics session, suitable for use in command-line
/// applications.
///
/// [analyticsUrl] is an optional replacement for the default Google Analytics
/// URL (`https://www.google-analytics.com/collect`).
///
/// `trackingId`, `applicationName`, and `applicationVersion` values should be supplied.
/// `analyticsUrl` is optional, and lets user's substitute their own analytics URL for
/// the default.
///
/// `documentDirectory` is where the analytics settings are stored. It
/// defaults to the user home directory. For regular `dart:io` apps this doesn't need to
/// be supplied. For Flutter applications, you should pass in a value like
/// `PathProvider.getApplicationDocumentsDirectory()`.
///
/// [batchingDelay] is used to control batching behaviour. Events will be sent
/// batches of 20 after the duration is over from when the first message was
/// sent. The default is 0 milliseconds, meaning that messages will be sent when
/// control returns to the event loop.
///
/// Batched messages are sent in batches of up to 20 messages. They will be sent
/// to [analyticsBatchingUrl] defaulting to
/// `https://www.google-analytics.com/batch`.
class AnalyticsIO extends AnalyticsImpl {
AnalyticsIO(
String trackingId,
String applicationName,
String applicationVersion, {
String? analyticsUrl,
String? analyticsBatchingUrl,
Directory? documentDirectory,
HttpClient? client,
Duration? batchingDelay,
}) : super(
trackingId,
IOPersistentProperties(applicationName,
documentDirPath: documentDirectory?.path),
IOPostHandler(client: client),
applicationName: applicationName,
applicationVersion: applicationVersion,
analyticsUrl: analyticsUrl,
analyticsBatchingUrl: analyticsBatchingUrl,
batchingDelay: batchingDelay,
) {
final locale = getPlatformLocale();
if (locale != null) {
setSessionValue('ul', locale);
}
}
}
@visibleForTesting
String createUserAgent() {
final locale = getPlatformLocale() ?? '';
if (Platform.isAndroid) {
return 'Mozilla/5.0 (Android; Mobile; ${locale})';
} else if (Platform.isIOS) {
return 'Mozilla/5.0 (iPhone; U; CPU iPhone OS like Mac OS X; ${locale})';
} else if (Platform.isMacOS) {
return 'Mozilla/5.0 (Macintosh; Intel Mac OS X; Macintosh; ${locale})';
} else if (Platform.isWindows) {
return 'Mozilla/5.0 (Windows; Windows; Windows; ${locale})';
} else if (Platform.isLinux) {
return 'Mozilla/5.0 (Linux; Linux; Linux; ${locale})';
} else {
// Dart/1.8.0 (macos; macos; macos; en_US)
var os = Platform.operatingSystem;
return 'Dart/${getDartVersion()} (${os}; ${os}; ${os}; ${locale})';
}
}
String userHomeDir() {
var envKey = Platform.operatingSystem == 'windows' ? 'APPDATA' : 'HOME';
var value = Platform.environment[envKey];
return value ?? '.';
}
String getDartVersion() {
var ver = Platform.version;
var index = ver.indexOf(' ');
if (index != -1) ver = ver.substring(0, index);
return ver;
}
class IOPostHandler extends PostHandler {
final HttpClient _client;
IOPostHandler({HttpClient? client})
: _client = (client ?? HttpClient())..userAgent = createUserAgent();
@override
String encodeHit(Map<String, String> hit) {
return postEncode(hit);
}
@override
Future sendPost(String url, List<String> batch) async {
var data = batch.join('\n');
try {
var req = await _client.postUrl(Uri.parse(url));
req.write(data);
var response = await req.close();
await response.drain();
} on Exception {
// Catch errors that can happen during a request, but that we can't do
// anything about, e.g. a missing internet connection.
}
}
@override
void close() {
// Do a force close to ensure that lingering requests will not stall the
// program.
_client.close(force: true);
}
}
JsonEncoder _jsonEncoder = JsonEncoder.withIndent(' ');
class IOPersistentProperties extends PersistentProperties {
late final File _file;
late Map _map;
IOPersistentProperties(String name, {String? documentDirPath}) : super(name) {
var fileName = '.${name.replaceAll(' ', '_')}';
documentDirPath ??= userHomeDir();
_file = File(path.join(documentDirPath, fileName));
if (!_file.existsSync()) {
_file.createSync();
}
syncSettings();
}
IOPersistentProperties.fromFile(File file) : super(path.basename(file.path)) {
_file = file;
if (!_file.existsSync()) {
_file.createSync();
}
syncSettings();
}
@override
dynamic operator [](String key) => _map[key];
@override
void operator []=(String key, dynamic value) {
if (value == null && !_map.containsKey(key)) return;
if (_map[key] == value) return;
if (value == null) {
_map.remove(key);
} else {
_map[key] = value;
}
try {
_file.writeAsStringSync(_jsonEncoder.convert(_map) + '\n');
} catch (_) {}
}
@override
void syncSettings() {
try {
var contents = _file.readAsStringSync();
if (contents.isEmpty) contents = '{}';
_map = jsonDecode(contents);
} catch (_) {
_map = {};
}
}
}
/// Return the string for the platform's locale; return's `null` if the locale
/// can't be determined.
String? getPlatformLocale() {
var locale = Platform.localeName;
// Convert `en_US.UTF-8` to `en_US`.
var index = locale.indexOf('.');
if (index != -1) locale = locale.substring(0, index);
// Convert `en_US` to `en-us`.
locale = locale.replaceAll('_', '-').toLowerCase();
return locale;
}