mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-05 16:15:43 +00:00
fix(analytics): use Android ID and persist UUID fallbacks (#1624)
This commit is contained in:
9 files changed
+61
-29
No files matched your search
@@ -196,7 +196,9 @@ class ExtraConfig {
|
||||
List<String> createHistory;
|
||||
bool runAsMenubarApp;
|
||||
bool analyticsEnabled;
|
||||
String analyticsClientId;
|
||||
|
||||
/// Persisted UUID fallback; IDs returned by platform APIs are not stored here.
|
||||
String deviceId;
|
||||
|
||||
ExtraConfig({
|
||||
this.themeMode = '',
|
||||
@@ -214,7 +216,7 @@ class ExtraConfig {
|
||||
this.createHistory = const [],
|
||||
this.runAsMenubarApp = false,
|
||||
this.analyticsEnabled = true,
|
||||
this.analyticsClientId = '',
|
||||
this.deviceId = '',
|
||||
}) : windowState = windowState ?? WindowStateConfig();
|
||||
|
||||
factory ExtraConfig.fromJson(Map<String, dynamic>? json) =>
|
||||
|
||||
@@ -193,7 +193,7 @@ ExtraConfig _$ExtraConfigFromJson(Map<String, dynamic> json) => ExtraConfig(
|
||||
createHistory: (json['createHistory'] as List<dynamic>?)?.map((e) => e as String).toList() ?? const [],
|
||||
runAsMenubarApp: json['runAsMenubarApp'] as bool? ?? false,
|
||||
analyticsEnabled: json['analyticsEnabled'] as bool? ?? true,
|
||||
analyticsClientId: json['analyticsClientId'] as String? ?? '',
|
||||
deviceId: json['deviceId'] as String? ?? '',
|
||||
);
|
||||
|
||||
Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => <String, dynamic>{
|
||||
@@ -212,7 +212,7 @@ Map<String, dynamic> _$ExtraConfigToJson(ExtraConfig instance) => <String, dynam
|
||||
'createHistory': instance.createHistory,
|
||||
'runAsMenubarApp': instance.runAsMenubarApp,
|
||||
'analyticsEnabled': instance.analyticsEnabled,
|
||||
'analyticsClientId': instance.analyticsClientId,
|
||||
'deviceId': instance.deviceId,
|
||||
};
|
||||
|
||||
WindowStateConfig _$WindowStateConfigFromJson(Map<String, dynamic> json) => WindowStateConfig(
|
||||
|
||||
@@ -255,9 +255,9 @@ class AppRuntimeController extends AsyncNotifier<AppRuntimeState> {
|
||||
Future<void> _initAnalytics(DownloaderConfig config) async {
|
||||
if (!AnalyticsConfig.isConfigured || !config.extra.analyticsEnabled) return;
|
||||
try {
|
||||
await Analytics.instance.init(fallbackClientId: config.extra.analyticsClientId);
|
||||
if (config.extra.analyticsClientId.isEmpty && Analytics.instance.clientId.isNotEmpty) {
|
||||
config.extra.analyticsClientId = Analytics.instance.clientId;
|
||||
final generatedClientId = await Analytics.instance.init(fallbackClientId: config.extra.deviceId);
|
||||
if (generatedClientId != null) {
|
||||
config.extra.deviceId = generatedClientId;
|
||||
await ref.read(gopeedServiceProvider).putConfig(config);
|
||||
}
|
||||
unawaited(Analytics.instance.logAppOpen());
|
||||
|
||||
@@ -56,7 +56,7 @@ class SettingsController extends AsyncNotifier<SettingsState> {
|
||||
..bookmarks = latest.extra.bookmarks
|
||||
..createHistory = latest.extra.createHistory
|
||||
..runAsMenubarApp = latest.extra.runAsMenubarApp
|
||||
..analyticsClientId = latest.extra.analyticsClientId;
|
||||
..deviceId = latest.extra.deviceId;
|
||||
// The backend preserves protocol-owned results and validates the whole PUT.
|
||||
await service.putConfig(config);
|
||||
final saved = await service.getConfig();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:android_id/android_id.dart';
|
||||
import 'package:device_info_plus/device_info_plus.dart';
|
||||
import 'package:dio/dio.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
|
||||
import 'log_util.dart';
|
||||
import 'package_info.dart';
|
||||
@@ -31,12 +33,23 @@ class Analytics {
|
||||
|
||||
String get clientId => _initialized ? _clientId : '';
|
||||
|
||||
Future<void> init({String fallbackClientId = ''}) async {
|
||||
if (_initialized || !AnalyticsConfig.isConfigured) return;
|
||||
_clientId = await _getDeviceId(fallbackClientId);
|
||||
/// Returns a newly generated fallback ID for the caller to persist, if any.
|
||||
Future<String?> init({String fallbackClientId = ''}) async {
|
||||
if (_initialized || !AnalyticsConfig.isConfigured) return null;
|
||||
final deviceId = await _getDeviceId();
|
||||
String? generatedClientId;
|
||||
if (deviceId != null && deviceId.isNotEmpty) {
|
||||
_clientId = deviceId;
|
||||
} else if (fallbackClientId.isNotEmpty) {
|
||||
_clientId = fallbackClientId;
|
||||
} else {
|
||||
_clientId = const Uuid().v4();
|
||||
generatedClientId = _clientId;
|
||||
}
|
||||
_sessionId = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
_dio = Dio(BaseOptions(connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10)));
|
||||
_initialized = true;
|
||||
return generatedClientId;
|
||||
}
|
||||
|
||||
Future<void> logAppOpen() => logEvent('app_open');
|
||||
@@ -70,14 +83,14 @@ class Analytics {
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> _getDeviceId(String fallbackClientId) async {
|
||||
Future<String?> _getDeviceId() async {
|
||||
if (kIsWeb) return null;
|
||||
|
||||
String? deviceId;
|
||||
try {
|
||||
final info = DeviceInfoPlugin();
|
||||
if (kIsWeb) {
|
||||
deviceId = null;
|
||||
} else if (Platform.isAndroid) {
|
||||
deviceId = (await info.androidInfo).id;
|
||||
if (Platform.isAndroid) {
|
||||
deviceId = await const AndroidId().getId();
|
||||
} else if (Platform.isIOS) {
|
||||
deviceId = (await info.iosInfo).identifierForVendor;
|
||||
} else if (Platform.isMacOS) {
|
||||
@@ -91,14 +104,6 @@ class Analytics {
|
||||
logger.w('GA4Analytics: failed to read device id: $error');
|
||||
}
|
||||
|
||||
if (deviceId == null || deviceId.isEmpty) {
|
||||
deviceId = fallbackClientId;
|
||||
if (deviceId.isEmpty) {
|
||||
final random = DateTime.now().microsecondsSinceEpoch % 2147483647;
|
||||
final timestamp = DateTime.now().millisecondsSinceEpoch ~/ 1000;
|
||||
deviceId = '$random.$timestamp';
|
||||
}
|
||||
}
|
||||
return deviceId;
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
android_id:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: android_id
|
||||
sha256: "543bbfcf316de69d3ac36601d74eeaacd0248178a2671b00ad30d09f35bd3581"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.5.2+1"
|
||||
animation_kit:
|
||||
dependency: transitive
|
||||
description:
|
||||
@@ -1621,7 +1629,7 @@ packages:
|
||||
source: hosted
|
||||
version: "3.1.5"
|
||||
uuid:
|
||||
dependency: transitive
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: uuid
|
||||
sha256: "9b129329f58692f6e6578329498a8fe9fbe98f090beb764ffbb8ee2eadd01dcd"
|
||||
|
||||
@@ -47,6 +47,8 @@ dependencies:
|
||||
package_info_plus: ^10.2.1
|
||||
pub_semver: ^2.2.0
|
||||
device_info_plus: ^13.2.0
|
||||
android_id: ^0.5.2+1
|
||||
uuid: ^4.6.0
|
||||
win32_registry: ^3.0.3
|
||||
launch_at_startup:
|
||||
git:
|
||||
|
||||
@@ -81,7 +81,7 @@ void main() {
|
||||
..createHistory = ['https://example.com/file.zip']
|
||||
..runAsMenubarApp = true
|
||||
..analyticsEnabled = false
|
||||
..analyticsClientId = 'client-id';
|
||||
..deviceId = 'client-id';
|
||||
|
||||
final decoded = DownloaderConfig.fromJson(config.toJson()).extra;
|
||||
expect(decoded.windowState.isMaximized, isTrue);
|
||||
@@ -91,7 +91,22 @@ void main() {
|
||||
expect(decoded.createHistory, ['https://example.com/file.zip']);
|
||||
expect(decoded.runAsMenubarApp, isTrue);
|
||||
expect(decoded.analyticsEnabled, isFalse);
|
||||
expect(decoded.analyticsClientId, 'client-id');
|
||||
expect(decoded.deviceId, 'client-id');
|
||||
});
|
||||
|
||||
test('legacy analytics client IDs are ignored without migration', () {
|
||||
final config = ExtraConfig.fromJson({'analyticsClientId': 'AP3A.241105.008'});
|
||||
|
||||
expect(config.deviceId, isEmpty);
|
||||
expect(config.toJson(), isNot(contains('analyticsClientId')));
|
||||
});
|
||||
|
||||
test('analytics fallback ID uses only its dedicated config field', () {
|
||||
final config = ExtraConfig.fromJson({'analyticsClientId': 'AP3A.241105.008', 'deviceId': 'stored-fallback'});
|
||||
|
||||
expect(config.deviceId, 'stored-fallback');
|
||||
expect(config.toJson()['deviceId'], 'stored-fallback');
|
||||
expect(config.toJson(), isNot(contains('analyticsClientId')));
|
||||
});
|
||||
|
||||
test('categories and the auto-categorize switch round-trip through the config', () {
|
||||
|
||||
@@ -44,7 +44,7 @@ void main() {
|
||||
final backend = DownloaderConfig()
|
||||
..extra.windowState = WindowStateConfig(isMaximized: true, width: 1200, height: 800)
|
||||
..extra.createHistory = ['https://example.com/existing.zip']
|
||||
..extra.analyticsClientId = 'existing-client';
|
||||
..extra.deviceId = 'existing-client';
|
||||
DownloaderConfig? saved;
|
||||
final registry = CapabilityRegistry(createAppCapabilityCodecs())
|
||||
..bind(GopeedMethods.getConfig, (_) => DownloaderConfig.fromJson(backend.toJson()))
|
||||
@@ -67,7 +67,7 @@ void main() {
|
||||
expect(saved?.downloadDir, 'E:/New');
|
||||
expect(saved?.extra.windowState.isMaximized, isTrue);
|
||||
expect(saved?.extra.createHistory, ['https://example.com/existing.zip']);
|
||||
expect(saved?.extra.analyticsClientId, 'existing-client');
|
||||
expect(saved?.extra.deviceId, 'existing-client');
|
||||
});
|
||||
|
||||
test('protocol edits use a single full PUT and reload backend-owned tracker results', () async {
|
||||
|
||||
Reference in new issue
Block a user