feat: add anonymous analytics (#1254)

This commit is contained in:
Levi authored and GitHub committed 2026-01-23 22:29:15 +08:00
1 parent 7c696ee6e0
commit d61da67456
7 files changed
+204 -10

No files matched your search

@@ -14,6 +14,7 @@ import 'package:window_manager/window_manager.dart';
import '../../../../api/api.dart' as api;
import '../../../../api/model/downloader_config.dart';
import '../../../../database/database.dart';
import '../../../../util/analytics.dart';
import '../../../../i18n/message.dart';
import '../../../../util/input_formatter.dart';
import '../../../../util/locale_manager.dart';
@@ -859,6 +860,21 @@ class SettingView extends GetView<SettingController> {
);
});
final analyticsEnabled = Database.instance.getAnalyticsEnabled().obs;
buildAnalyticsEnabled() {
return ListTile(
title: Text('analyticsEnabled'.tr),
subtitle: Text('analyticsEnabledDesc'.tr),
trailing: Obx(() => Switch(
value: analyticsEnabled.value,
onChanged: (bool value) {
analyticsEnabled.value = value;
Database.instance.saveAnalyticsEnabled(value);
},
)),
);
}
buildThanks() {
const thankPage =
'https://github.com/GopeedLab/gopeed/graphs/contributors';
@@ -1538,6 +1554,7 @@ class SettingView extends GetView<SettingController> {
buildHomepage(),
buildVersion(),
buildAutoCheckUpdate(),
if (Config.isConfigured) buildAnalyticsEnabled(),
buildThanks(),
]),
)),
+18
View File
@@ -11,6 +11,8 @@ const String _bookmark = 'bookmark';
const String _createHistory = 'createHistory';
const String _webToken = 'webToken';
const String _runAsMenubarApp = 'runAsMenubarApp';
const String _analyticsEnabled = 'analyticsEnabled';
const String _analyticsClientId = 'analyticsClientId';
class Database {
static final Database _instance = Database._internal();
@@ -118,4 +120,20 @@ class Database {
bool getRunAsMenubarApp() {
return box.get(_runAsMenubarApp, defaultValue: false) as bool;
}
void saveAnalyticsEnabled(bool value) {
box.put(_analyticsEnabled, value);
}
bool getAnalyticsEnabled() {
return box.get(_analyticsEnabled, defaultValue: true) as bool;
}
void saveAnalyticsClientId(String clientId) {
box.put(_analyticsClientId, clientId);
}
String? getAnalyticsClientId() {
return box.get(_analyticsClientId) as String?;
}
}
+2
View File
@@ -59,6 +59,8 @@ const enUS = {
'themeDark': 'Dark',
'locale': 'Language',
'notifyWhenNewVersion': 'Notify for updates',
'analyticsEnabled': 'Upload Statistics',
'analyticsEnabledDesc': 'Share anonymous usage data to help us improve',
'about': 'About',
'homepage': 'Homepage',
'version': 'Version',
+2
View File
@@ -57,6 +57,8 @@ const zhCN = {
'themeDark': '暗黑主题',
'locale': '语言',
'notifyWhenNewVersion': '有新版本时提醒',
'analyticsEnabled': '上传统计数据',
'analyticsEnabledDesc': '分享匿名使用数据以帮助我们改进',
'about': '关于',
'homepage': '主页',
'version': '版本',
+10
View File
@@ -4,6 +4,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_foreground_task/flutter_foreground_task.dart';
import 'package:get/get.dart';
import 'package:gopeed/util/analytics.dart';
import 'package:hotkey_manager/hotkey_manager.dart';
import 'package:window_manager/window_manager.dart';
@@ -195,4 +196,13 @@ Future<void> onStart() async {
}
});
}
if (Config.isConfigured && Database.instance.getAnalyticsEnabled()) {
try {
await Analytics.instance.init();
await Analytics.instance.logAppOpen();
} catch (e) {
logger.w("GA4 init failed: $e");
}
}
}
+137
View File
@@ -0,0 +1,137 @@
import 'dart:io';
import 'package:device_info_plus/device_info_plus.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:gopeed/database/database.dart';
import 'package:gopeed/util/package_info.dart';
import 'log_util.dart';
/// GA4 Measurement Protocol configuration from dart-define
class Config {
static const String measurementId =
String.fromEnvironment('GA4_MEASUREMENT_ID');
static const String apiSecret = String.fromEnvironment('GA4_API_SECRET');
static bool get isConfigured =>
measurementId.isNotEmpty && apiSecret.isNotEmpty;
}
/// GA4 Measurement Protocol Analytics
class Analytics {
static final Analytics _instance = Analytics._internal();
static Analytics get instance => _instance;
Analytics._internal();
late String _clientId;
late int _sessionId;
late Dio _dio;
bool _initialized = false;
Future<void> init() async {
if (_initialized) return;
if (!Config.isConfigured) return;
_clientId = await _getDeviceId();
_dio = Dio(BaseOptions(
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
));
_sessionId = DateTime.now().millisecondsSinceEpoch ~/ 1000;
_initialized = true;
}
Future<String> _getDeviceId() async {
final deviceInfo = DeviceInfoPlugin();
String? deviceId;
try {
if (kIsWeb) {
deviceId = null;
} else if (Platform.isAndroid) {
final androidInfo = await deviceInfo.androidInfo;
deviceId = androidInfo.id;
} else if (Platform.isIOS) {
final iosInfo = await deviceInfo.iosInfo;
deviceId = iosInfo.identifierForVendor;
} else if (Platform.isMacOS) {
final macInfo = await deviceInfo.macOsInfo;
deviceId = macInfo.systemGUID;
} else if (Platform.isWindows) {
final windowsInfo = await deviceInfo.windowsInfo;
deviceId = windowsInfo.deviceId;
} else if (Platform.isLinux) {
final linuxInfo = await deviceInfo.linuxInfo;
deviceId = linuxInfo.machineId;
}
} catch (e) {
debugPrint('GA4Analytics: Failed to get device id: $e');
}
// Fallback to persisted client id
if (deviceId == null || deviceId.isEmpty) {
deviceId = Database.instance.getAnalyticsClientId();
if (deviceId == null || deviceId.isEmpty) {
final random = DateTime.now().microsecondsSinceEpoch % 2147483647;
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
deviceId = '$random.$timestamp';
Database.instance.saveAnalyticsClientId(deviceId);
}
}
return deviceId;
}
String _getPlatform() {
if (kIsWeb) return 'web';
if (Platform.isAndroid) return 'android';
if (Platform.isIOS) return 'ios';
if (Platform.isMacOS) return 'macos';
if (Platform.isWindows) return 'windows';
if (Platform.isLinux) return 'linux';
return 'unknown';
}
Future<void> logAppOpen() async {
await logEvent('app_open');
}
Future<void> logEvent(String name, [Map<String, dynamic>? params]) async {
if (!_initialized) {
return;
}
final eventParams = <String, dynamic>{
'session_id': _sessionId.toString(),
'engagement_time_msec': 100,
'platform': _getPlatform(),
'app_version': packageInfo.version,
...?params,
};
final payload = {
'client_id': _clientId,
'timestamp_micros': DateTime.now().microsecondsSinceEpoch,
'events': [
{
'name': name,
'params': eventParams,
}
],
};
try {
await _dio.post(
'https://www.google-analytics.com/mp/collect',
queryParameters: {
'measurement_id': Config.measurementId,
'api_secret': Config.apiSecret,
},
data: payload,
);
} catch (e) {
logger.w('GA4Analytics: Failed to send event "$name": $e');
}
}
}