mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-06 00:25:43 +00:00
feat: add anonymous analytics (#1254)
This commit is contained in:
7 files changed
+204
-10
No files matched your search
+18
-10
@@ -15,6 +15,8 @@ on:
|
||||
env:
|
||||
GO_VERSION: "1.24"
|
||||
FLUTTER_VERSION: "3.38.7"
|
||||
GA4_MEASUREMENT_ID: ${{ secrets.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ secrets.GA4_API_SECRET }}
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
@@ -97,7 +99,7 @@ jobs:
|
||||
go build -ldflags="-w -s" -o ui/flutter/assets/exec/updater.exe github.com/GopeedLab/gopeed/cmd/updater
|
||||
cd ui/flutter
|
||||
$TAG = "v$env:VERSION"
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable"
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsPortable" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
$system = "C:\Windows\System32"
|
||||
if ("${{ matrix.arch }}" -eq "amd64") {
|
||||
# for amd64
|
||||
@@ -119,7 +121,7 @@ jobs:
|
||||
New-Item -Path build\windows\Output -ItemType Directory
|
||||
Compress-Archive -Path "$release*" -DestinationPath "build\windows\Output\Gopeed-$TAG-windows-${{ matrix.arch }}-portable.zip"
|
||||
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller"
|
||||
flutter build windows --dart-define="UPDATE_CHANNEL=windowsInstaller" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
cd build/windows
|
||||
echo @"
|
||||
; Script generated by the Inno Setup Script Wizard.
|
||||
@@ -189,7 +191,7 @@ jobs:
|
||||
overwrite: true
|
||||
build-macos-amd64-lib:
|
||||
if: ${{ github.event.inputs.platform == 'all' || github.event.inputs.platform == 'macos' }}
|
||||
runs-on: macos-13
|
||||
runs-on: macos-15-intel
|
||||
needs: [get-release]
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
@@ -274,7 +276,7 @@ jobs:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg"
|
||||
flutter build macos --dart-define="UPDATE_CHANNEL=macosDmg" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
cd build/macos/Build/Products/Release
|
||||
cat>appdmg.json<<EOF
|
||||
{
|
||||
@@ -368,6 +370,8 @@ jobs:
|
||||
build_args:
|
||||
dart-define:
|
||||
UPDATE_CHANNEL: linuxAppImage
|
||||
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
|
||||
- name: deb
|
||||
package:
|
||||
platform: linux
|
||||
@@ -375,6 +379,8 @@ jobs:
|
||||
build_args:
|
||||
dart-define:
|
||||
UPDATE_CHANNEL: linuxDeb
|
||||
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
|
||||
- name: rpm
|
||||
package:
|
||||
platform: linux
|
||||
@@ -382,6 +388,8 @@ jobs:
|
||||
build_args:
|
||||
dart-define:
|
||||
UPDATE_CHANNEL: linuxRpm
|
||||
GA4_MEASUREMENT_ID: ${{ env.GA4_MEASUREMENT_ID }}
|
||||
GA4_API_SECRET: ${{ env.GA4_API_SECRET }}
|
||||
EOF
|
||||
dart pub global activate -sgit https://github.com/GopeedLab/flutter_distributor.git --git-path packages/flutter_distributor
|
||||
flutter_distributor release --name linux
|
||||
@@ -499,7 +507,7 @@ jobs:
|
||||
set +e
|
||||
dart pub get
|
||||
set -eux
|
||||
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap"
|
||||
flutter build linux --dart-define="UPDATE_CHANNEL=linuxSnap" --dart-define="GA4_MEASUREMENT_ID=$GA4_MEASUREMENT_ID" --dart-define="GA4_API_SECRET=$GA4_API_SECRET"
|
||||
mkdir -p \$CRAFT_PART_INSTALL/bin/
|
||||
cp -r build/linux/*/release/bundle/* \$CRAFT_PART_INSTALL/bin/
|
||||
gpu-2404:
|
||||
@@ -564,8 +572,8 @@ jobs:
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -checklinkname=0 -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/android/app/libs/libgopeed.aar -target=android -androidapi 21 -javapkg=com.gopeed github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
echo $APK_KEYSTORE | base64 -di > android/app/upload-keystore.jks
|
||||
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk"
|
||||
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk"
|
||||
flutter build apk --dart-define="UPDATE_CHANNEL=androidApk" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
flutter build apk --split-per-abi --dart-define="UPDATE_CHANNEL=androidApk" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
mkdir dist
|
||||
cp build/app/outputs/flutter-apk/app-release.apk dist/Gopeed-v$VERSION-android.apk
|
||||
cp build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk dist/Gopeed-v$VERSION-android-armeabi-v7a.apk
|
||||
@@ -598,7 +606,7 @@ jobs:
|
||||
gomobile init
|
||||
gomobile bind -tags nosqlite -ldflags="-w -s -X github.com/GopeedLab/gopeed/pkg/base.Version=$VERSION" -o ui/flutter/ios/Frameworks/Libgopeed.xcframework -target=ios github.com/GopeedLab/gopeed/bind/mobile
|
||||
cd ui/flutter
|
||||
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa"
|
||||
flutter build ios --no-codesign --dart-define="UPDATE_CHANNEL=iosIpa" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
mkdir Payload
|
||||
cp -r build/ios/iphoneos/Runner.app Payload
|
||||
zip -r -y Payload.zip Payload/Runner.app
|
||||
@@ -627,7 +635,7 @@ jobs:
|
||||
VERSION: ${{ needs.get-release.outputs.version }}
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn
|
||||
flutter build web --no-web-resources-cdn --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
dart ../../.github/workflows/scripts/flutter_local_font.dart
|
||||
cd ../../
|
||||
cp -r ui/flutter/build/web cmd/web/dist
|
||||
@@ -744,7 +752,7 @@ jobs:
|
||||
- name: Build flutter web
|
||||
run: |
|
||||
cd ui/flutter
|
||||
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker"
|
||||
flutter build web --no-web-resources-cdn --dart-define="UPDATE_CHANNEL=docker" --dart-define="GA4_MEASUREMENT_ID=${{ env.GA4_MEASUREMENT_ID }}" --dart-define="GA4_API_SECRET=${{ env.GA4_API_SECRET }}"
|
||||
dart ../../.github/workflows/scripts/flutter_local_font.dart
|
||||
cd ../../
|
||||
rm -rf cmd/web/dist
|
||||
|
||||
@@ -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(),
|
||||
]),
|
||||
)),
|
||||
|
||||
@@ -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?;
|
||||
}
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -57,6 +57,8 @@ const zhCN = {
|
||||
'themeDark': '暗黑主题',
|
||||
'locale': '语言',
|
||||
'notifyWhenNewVersion': '有新版本时提醒',
|
||||
'analyticsEnabled': '上传统计数据',
|
||||
'analyticsEnabledDesc': '分享匿名使用数据以帮助我们改进',
|
||||
'about': '关于',
|
||||
'homepage': '主页',
|
||||
'version': '版本',
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user