# Flutter 设备信息收集与上报技术方案 > 用于物理设备调试信息收集,支持后续兼容性分析与优化 > 适用场景: Flutter Android App 开发,无法在每台手机上验证的细节问题 > **扩展功能: 日活 DAU 统计 + 实时在线人数统计** --- ## 一、方案概述 ### 1.1 核心目标 - 自动收集物理设备的运行时信息 - 结构化上报到后端,供后续分析 - 定位特定机型/系统的兼容性问题 - 收集性能数据和用户行为轨迹 - **统计日活 DAU(按自然日去重活跃用户)** - **统计实时在线人数(3分钟窗口判定)** ### 1.2 技术栈 ```yaml # 核心依赖 dependencies: # 设备信息采集 device_info_plus: ^11.0.0 package_info_plus: ^8.0.0 # 网络请求 dio: ^5.4.0 # 本地存储 shared_preferences: ^2.2.0 sqflite: ^2.3.0 # UUID生成 uuid: ^4.2.1 # 错误监控 (可选三选一) firebase_crashlytics: ^3.4.0 # 推荐:免费且功能强大 # sentry_flutter: ^7.0.0 # 备选:自托管或SaaS # JSON序列化 json_annotation: ^4.8.1 dev_dependencies: json_serializable: ^6.7.1 build_runner: ^2.4.0 ``` ### 1.3 架构设计 ``` lib/ ├── core/ │ └── telemetry/ │ ├── telemetry_service.dart # 核心服务 │ ├── models/ │ │ ├── telemetry_event.dart # 事件模型 │ │ ├── device_context.dart # 设备上下文 │ │ ├── performance_metric.dart # 性能指标 │ │ └── telemetry_config.dart # 远程配置模型 │ ├── collectors/ │ │ ├── device_info_collector.dart # 设备信息收集 │ │ ├── error_collector.dart # 错误收集 │ │ └── performance_collector.dart # 性能收集 │ ├── storage/ │ │ └── telemetry_storage.dart # 本地缓存 │ ├── uploader/ │ │ └── telemetry_uploader.dart # 批量上报 │ ├── session/ # 🆕 会话管理(用于DAU) │ │ ├── session_manager.dart # 会话生命周期管理 │ │ └── session_events.dart # 会话事件常量 │ └── presence/ # 🆕 在线状态(用于在线人数) │ ├── heartbeat_service.dart # 心跳服务 │ └── presence_config.dart # 心跳配置 ``` ### 1.4 DAU & 在线统计指标定义 #### 日活 DAU 定义 - **统计周期**: 某自然日(0:00–24:00),以服务器 `Asia/Shanghai` 时区为准 - **有效行为**: App 会话开始事件 `app_session_start` - **去重逻辑**: 按 `COALESCE(user_id, install_id)` 去重 - 登录用户:使用 `user_id` - 未登录用户:使用 `install_id`(首次安装生成的 UUID) #### 同时在线人数定义 - **时间窗口**: 3 分钟(180 秒) - **心跳频率**: 客户端前台状态下每 **60 秒** 上报一次心跳 - **判断规则**: 若 `now - last_heartbeat_time <= 180 秒`,则认为该用户在线 - **统计对象**: **仅登录用户**(未登录用户不参与在线统计) --- ## 二、核心模块实现 ### 2.1 设备上下文收集器 **文件: `lib/core/telemetry/models/device_context.dart`** ```dart import 'package:json_annotation/json_annotation.dart'; part 'device_context.g.dart'; @JsonSerializable() class DeviceContext { // 设备信息 final String platform; // 'android' | 'ios' final String brand; // 'Samsung' final String model; // 'SM-G9980' final String manufacturer; // 'samsung' final bool isPhysicalDevice; // true // 系统信息 final String osVersion; // '14' final int sdkInt; // 34 final String androidId; // 匿名设备ID // 屏幕信息 final ScreenInfo screen; // App信息 final String appName; final String packageName; final String appVersion; final String buildNumber; final String buildMode; // 'debug' | 'profile' | 'release' // 用户环境 final String locale; // 'zh_CN' final String timezone; // 'Asia/Shanghai' final bool isDarkMode; final String networkType; // 'wifi' | 'cellular' | 'none' // 时间戳 final DateTime collectedAt; DeviceContext({ required this.platform, required this.brand, required this.model, required this.manufacturer, required this.isPhysicalDevice, required this.osVersion, required this.sdkInt, required this.androidId, required this.screen, required this.appName, required this.packageName, required this.appVersion, required this.buildNumber, required this.buildMode, required this.locale, required this.timezone, required this.isDarkMode, required this.networkType, required this.collectedAt, }); factory DeviceContext.fromJson(Map json) => _$DeviceContextFromJson(json); Map toJson() => _$DeviceContextToJson(this); } @JsonSerializable() class ScreenInfo { final double widthPx; final double heightPx; final double density; final double widthDp; final double heightDp; final bool hasNotch; ScreenInfo({ required this.widthPx, required this.heightPx, required this.density, required this.widthDp, required this.heightDp, required this.hasNotch, }); factory ScreenInfo.fromJson(Map json) => _$ScreenInfoFromJson(json); Map toJson() => _$ScreenInfoToJson(this); } ``` **文件: `lib/core/telemetry/collectors/device_info_collector.dart`** ```dart import 'dart:io'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart'; import 'package:flutter/material.dart'; import '../models/device_context.dart'; class DeviceInfoCollector { static DeviceInfoCollector? _instance; DeviceInfoCollector._(); factory DeviceInfoCollector() { _instance ??= DeviceInfoCollector._(); return _instance!; } DeviceContext? _cachedContext; /// 收集完整设备上下文(首次会缓存) Future collect(BuildContext context) async { if (_cachedContext != null) return _cachedContext!; final deviceInfo = DeviceInfoPlugin(); final packageInfo = await PackageInfo.fromPlatform(); final mediaQuery = MediaQuery.of(context); DeviceContext result; if (Platform.isAndroid) { final androidInfo = await deviceInfo.androidInfo; result = DeviceContext( platform: 'android', brand: androidInfo.brand, model: androidInfo.model, manufacturer: androidInfo.manufacturer, isPhysicalDevice: androidInfo.isPhysicalDevice, osVersion: androidInfo.version.release, sdkInt: androidInfo.version.sdkInt, androidId: androidInfo.id, // 匿名ID,不是IMEI screen: _collectScreenInfo(mediaQuery, androidInfo), appName: packageInfo.appName, packageName: packageInfo.packageName, appVersion: packageInfo.version, buildNumber: packageInfo.buildNumber, buildMode: _getBuildMode(), locale: Platform.localeName, timezone: DateTime.now().timeZoneName, isDarkMode: mediaQuery.platformBrightness == Brightness.dark, networkType: 'unknown', // 需要额外的connectivity包 collectedAt: DateTime.now(), ); } else { // iOS 实现类似 throw UnimplementedError('iOS support coming soon'); } _cachedContext = result; return result; } ScreenInfo _collectScreenInfo( MediaQueryData mediaQuery, AndroidDeviceInfo androidInfo, ) { final size = mediaQuery.size; final density = mediaQuery.devicePixelRatio; return ScreenInfo( widthPx: (size.width * density), heightPx: (size.height * density), density: density, widthDp: size.width, heightDp: size.height, hasNotch: mediaQuery.padding.top > 24, // 简单判断刘海屏 ); } String _getBuildMode() { if (kReleaseMode) return 'release'; if (kProfileMode) return 'profile'; return 'debug'; } /// 清除缓存(版本更新时调用) void clearCache() { _cachedContext = null; } } ``` ### 2.2 事件模型 **文件: `lib/core/telemetry/models/telemetry_event.dart`** ```dart import 'package:json_annotation/json_annotation.dart'; part 'telemetry_event.g.dart'; enum EventLevel { debug, info, warning, error, fatal, } enum EventType { pageView, // 页面访问 userAction, // 用户行为 apiCall, // API请求 performance, // 性能指标 error, // 错误异常 crash, // 崩溃 session, // 🆕 会话事件 (app_session_start, app_session_end) presence, // 🆕 在线状态 (心跳相关) } @JsonSerializable() class TelemetryEvent { final String eventId; // UUID final EventType type; final EventLevel level; final String name; // 事件名: 'app_session_start', 'open_planting_page' final Map? properties; // 事件参数 final DateTime timestamp; final String? userId; // 可选: 用户ID(登录后设置) final String? sessionId; // 会话ID final String installId; // 🆕 安装ID(设备唯一标识) // 关联设备信息(可以存ID或直接嵌入) final String deviceContextId; TelemetryEvent({ required this.eventId, required this.type, required this.level, required this.name, this.properties, required this.timestamp, this.userId, this.sessionId, required this.installId, required this.deviceContextId, }); factory TelemetryEvent.fromJson(Map json) => _$TelemetryEventFromJson(json); Map toJson() => _$TelemetryEventToJson(this); } ``` ### 2.3 本地存储 **文件: `lib/core/telemetry/storage/telemetry_storage.dart`** ```dart import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import '../models/telemetry_event.dart'; class TelemetryStorage { static const String _keyEventQueue = 'telemetry_event_queue'; static const String _keyDeviceContext = 'telemetry_device_context'; static const String _keyInstallId = 'telemetry_install_id'; static const int _maxQueueSize = 500; // 最多缓存500条 late SharedPreferences _prefs; Future init() async { _prefs = await SharedPreferences.getInstance(); } /// 保存设备上下文 Future saveDeviceContext(Map context) async { await _prefs.setString(_keyDeviceContext, jsonEncode(context)); } /// 读取设备上下文 Map? getDeviceContext() { final str = _prefs.getString(_keyDeviceContext); if (str == null) return null; return jsonDecode(str) as Map; } /// 🆕 保存 InstallId Future saveInstallId(String installId) async { await _prefs.setString(_keyInstallId, installId); } /// 🆕 读取 InstallId String? getInstallId() { return _prefs.getString(_keyInstallId); } /// 添加事件到队列 Future enqueueEvent(TelemetryEvent event) async { final queue = _getEventQueue(); // 防止队列过大 if (queue.length >= _maxQueueSize) { queue.removeAt(0); // 移除最旧的 } queue.add(event.toJson()); await _saveEventQueue(queue); } /// 批量添加事件 Future enqueueEvents(List events) async { final queue = _getEventQueue(); for (var event in events) { if (queue.length >= _maxQueueSize) break; queue.add(event.toJson()); } await _saveEventQueue(queue); } /// 获取待上传的事件(最多N条) List dequeueEvents(int limit) { final queue = _getEventQueue(); final count = queue.length > limit ? limit : queue.length; final events = queue .take(count) .map((json) => TelemetryEvent.fromJson(json)) .toList(); return events; } /// 删除已上传的事件 Future removeEvents(int count) async { final queue = _getEventQueue(); if (count >= queue.length) { await clearEventQueue(); } else { queue.removeRange(0, count); await _saveEventQueue(queue); } } /// 获取队列长度 int getQueueSize() { return _getEventQueue().length; } /// 清空事件队列 Future clearEventQueue() async { await _prefs.remove(_keyEventQueue); } // 私有方法 List> _getEventQueue() { final str = _prefs.getString(_keyEventQueue); if (str == null) return []; final List list = jsonDecode(str); return list.cast>(); } Future _saveEventQueue(List> queue) async { await _prefs.setString(_keyEventQueue, jsonEncode(queue)); } } ``` ### 2.4 批量上报器 **文件: `lib/core/telemetry/uploader/telemetry_uploader.dart`** ```dart import 'dart:async'; import 'package:dio/dio.dart'; import '../models/telemetry_event.dart'; import '../storage/telemetry_storage.dart'; class TelemetryUploader { final String apiBaseUrl; final TelemetryStorage storage; final Dio _dio; Timer? _uploadTimer; bool _isUploading = false; /// 🆕 获取认证头的回调 Map Function()? getAuthHeaders; TelemetryUploader({ required this.apiBaseUrl, required this.storage, this.getAuthHeaders, }) : _dio = Dio(BaseOptions( baseUrl: apiBaseUrl, connectTimeout: const Duration(seconds: 10), receiveTimeout: const Duration(seconds: 10), )); /// 启动定时上传(每30秒或累积20条) void startPeriodicUpload({ Duration interval = const Duration(seconds: 30), int batchSize = 20, }) { _uploadTimer?.cancel(); _uploadTimer = Timer.periodic(interval, (_) { uploadIfNeeded(batchSize: batchSize); }); } /// 停止定时上传 void stopPeriodicUpload() { _uploadTimer?.cancel(); _uploadTimer = null; } /// 条件上传(队列大于阈值才上传) Future uploadIfNeeded({int batchSize = 20}) async { if (_isUploading) return; final queueSize = storage.getQueueSize(); if (queueSize < 10) return; // 少于10条不上传,等待积累 await uploadBatch(batchSize: batchSize); } /// 立即上传一批 Future uploadBatch({int batchSize = 20}) async { if (_isUploading) return false; _isUploading = true; try { final events = storage.dequeueEvents(batchSize); if (events.isEmpty) return true; // 调用后端API final response = await _dio.post( '/api/v1/analytics/events', data: { 'events': events.map((e) => e.toJson()).toList(), }, options: Options( headers: getAuthHeaders?.call(), ), ); if (response.statusCode == 200) { // 上传成功,删除本地记录 await storage.removeEvents(events.length); print('✅ Uploaded ${events.length} events'); return true; } else { print('❌ Upload failed: ${response.statusCode}'); return false; } } catch (e) { print('❌ Upload error: $e'); return false; } finally { _isUploading = false; } } /// 强制上传全部(app退出前调用) Future forceUploadAll() async { stopPeriodicUpload(); while (storage.getQueueSize() > 0) { final success = await uploadBatch(batchSize: 50); if (!success) break; // 失败则放弃,下次启动再传 } } } ``` ### 2.5 会话管理模块(用于 DAU) **文件: `lib/core/telemetry/session/session_events.dart`** ```dart /// 会话相关的事件名常量 class SessionEvents { /// App 会话开始(用于 DAU 统计) /// 触发时机:App 从后台切到前台,或冷启动 static const String sessionStart = 'app_session_start'; /// App 会话结束 /// 触发时机:App 进入后台 static const String sessionEnd = 'app_session_end'; /// 心跳事件(用于在线统计) /// 触发时机:前台状态下每 60 秒 static const String heartbeat = 'presence_heartbeat'; /// 私有构造函数,防止实例化 SessionEvents._(); } /// 会话状态 enum SessionState { /// 前台活跃 foreground, /// 后台 background, /// 未知(初始状态) unknown, } ``` **文件: `lib/core/telemetry/session/session_manager.dart`** ```dart import 'dart:async'; import 'package:flutter/widgets.dart'; import 'package:uuid/uuid.dart'; import '../telemetry_service.dart'; import '../models/telemetry_event.dart'; import 'session_events.dart'; /// 会话管理器 /// /// 职责: /// 1. 监听 App 生命周期,触发 app_session_start/app_session_end 事件 /// 2. 管理 sessionId(每次前台生成新的) /// 3. 与 HeartbeatService 联动(前台启动心跳,后台停止) class SessionManager with WidgetsBindingObserver { static SessionManager? _instance; SessionManager._(); factory SessionManager() { _instance ??= SessionManager._(); return _instance!; } /// 当前会话 ID String? _currentSessionId; String? get currentSessionId => _currentSessionId; /// 当前会话状态 SessionState _state = SessionState.unknown; SessionState get state => _state; /// 会话开始时间 DateTime? _sessionStartTime; /// 回调:会话开始(HeartbeatService 监听此回调) VoidCallback? onSessionStart; /// 回调:会话结束(HeartbeatService 监听此回调) VoidCallback? onSessionEnd; /// TelemetryService 引用 TelemetryService? _telemetryService; /// 初始化 void initialize(TelemetryService telemetryService) { _telemetryService = telemetryService; WidgetsBinding.instance.addObserver(this); // 首次启动视为进入前台 _handleForeground(); } /// 销毁 void dispose() { WidgetsBinding.instance.removeObserver(this); _instance = null; } @override void didChangeAppLifecycleState(AppLifecycleState state) { switch (state) { case AppLifecycleState.resumed: _handleForeground(); break; case AppLifecycleState.paused: _handleBackground(); break; case AppLifecycleState.inactive: case AppLifecycleState.detached: case AppLifecycleState.hidden: // 不处理这些中间状态 break; } } /// 处理进入前台 void _handleForeground() { if (_state == SessionState.foreground) return; _state = SessionState.foreground; _startNewSession(); } /// 处理进入后台 void _handleBackground() { if (_state == SessionState.background) return; _state = SessionState.background; _endCurrentSession(); } /// 开始新会话 void _startNewSession() { // 生成新的 sessionId _currentSessionId = const Uuid().v4(); _sessionStartTime = DateTime.now(); // 记录 app_session_start 事件(用于 DAU) _telemetryService?.logEvent( SessionEvents.sessionStart, type: EventType.session, level: EventLevel.info, properties: { 'session_id': _currentSessionId, }, ); // 通知外部(HeartbeatService 会监听这个回调) onSessionStart?.call(); debugPrint('📱 [Session] Started: $_currentSessionId'); } /// 结束当前会话 void _endCurrentSession() { if (_currentSessionId == null) return; final duration = _sessionStartTime != null ? DateTime.now().difference(_sessionStartTime!).inSeconds : 0; // 记录 app_session_end 事件 _telemetryService?.logEvent( SessionEvents.sessionEnd, type: EventType.session, level: EventLevel.info, properties: { 'session_id': _currentSessionId, 'duration_seconds': duration, }, ); // 通知外部(HeartbeatService 会监听这个回调) onSessionEnd?.call(); debugPrint('📱 [Session] Ended: $_currentSessionId (${duration}s)'); _currentSessionId = null; _sessionStartTime = null; } /// 获取当前会话时长(秒) int get sessionDurationSeconds { if (_sessionStartTime == null) return 0; return DateTime.now().difference(_sessionStartTime!).inSeconds; } } ``` ### 2.6 心跳模块(用于在线人数) **文件: `lib/core/telemetry/presence/presence_config.dart`** ```dart /// 心跳配置 class PresenceConfig { /// 心跳间隔(秒) /// 默认 60 秒,与后端 3 分钟窗口配合 final int heartbeatIntervalSeconds; /// 是否仅登录用户发送心跳 /// 默认 true,未登录用户不参与在线统计 final bool requiresAuth; /// 是否启用心跳 final bool enabled; const PresenceConfig({ this.heartbeatIntervalSeconds = 60, this.requiresAuth = true, this.enabled = true, }); /// 默认配置 static const PresenceConfig defaultConfig = PresenceConfig(); /// 从远程配置解析 factory PresenceConfig.fromJson(Map json) { return PresenceConfig( heartbeatIntervalSeconds: json['heartbeat_interval_seconds'] ?? 60, requiresAuth: json['requires_auth'] ?? true, enabled: json['presence_enabled'] ?? true, ); } Map toJson() { return { 'heartbeat_interval_seconds': heartbeatIntervalSeconds, 'requires_auth': requiresAuth, 'presence_enabled': enabled, }; } } ``` **文件: `lib/core/telemetry/presence/heartbeat_service.dart`** ```dart import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:dio/dio.dart'; import '../session/session_manager.dart'; import '../session/session_events.dart'; import 'presence_config.dart'; /// 心跳服务 /// /// 职责: /// 1. 在 App 前台时定期发送心跳 /// 2. 进入后台时停止心跳 /// 3. 心跳失败时不立即重试,等待下一个周期 class HeartbeatService { static HeartbeatService? _instance; HeartbeatService._(); factory HeartbeatService() { _instance ??= HeartbeatService._(); return _instance!; } /// 配置 PresenceConfig _config = PresenceConfig.defaultConfig; /// 心跳定时器 Timer? _heartbeatTimer; /// 是否正在运行 bool _isRunning = false; bool get isRunning => _isRunning; /// 最后一次心跳时间 DateTime? _lastHeartbeatAt; DateTime? get lastHeartbeatAt => _lastHeartbeatAt; /// 心跳计数(调试用) int _heartbeatCount = 0; int get heartbeatCount => _heartbeatCount; /// API 基础地址 String? _apiBaseUrl; /// 获取 installId 的回调 String Function()? getInstallId; /// 获取 userId 的回调 String? Function()? getUserId; /// 获取 appVersion 的回调 String Function()? getAppVersion; /// 获取认证头的回调 Map Function()? getAuthHeaders; late Dio _dio; /// 初始化 void initialize({ required String apiBaseUrl, PresenceConfig? config, required String Function() getInstallId, required String? Function() getUserId, required String Function() getAppVersion, Map Function()? getAuthHeaders, }) { _apiBaseUrl = apiBaseUrl; _config = config ?? PresenceConfig.defaultConfig; this.getInstallId = getInstallId; this.getUserId = getUserId; this.getAppVersion = getAppVersion; this.getAuthHeaders = getAuthHeaders; _dio = Dio(BaseOptions( baseUrl: apiBaseUrl, connectTimeout: const Duration(seconds: 5), receiveTimeout: const Duration(seconds: 5), )); // 监听会话状态变化 final sessionManager = SessionManager(); sessionManager.onSessionStart = _onSessionStart; sessionManager.onSessionEnd = _onSessionEnd; // 如果当前已经在前台,立即启动心跳 if (sessionManager.state == SessionState.foreground) { _startHeartbeat(); } debugPrint('💓 [Heartbeat] Initialized, interval: ${_config.heartbeatIntervalSeconds}s'); } /// 更新配置(支持远程配置热更新) void updateConfig(PresenceConfig config) { final wasRunning = _isRunning; if (wasRunning) { _stopHeartbeat(); } _config = config; if (wasRunning && _config.enabled) { _startHeartbeat(); } } /// 销毁 void dispose() { _stopHeartbeat(); _instance = null; } /// 会话开始回调 void _onSessionStart() { if (_config.enabled) { _startHeartbeat(); } } /// 会话结束回调 void _onSessionEnd() { _stopHeartbeat(); } /// 启动心跳 void _startHeartbeat() { if (_isRunning) return; if (!_config.enabled) return; _isRunning = true; _heartbeatCount = 0; // 立即发送第一次心跳 _sendHeartbeat(); // 启动定时器 _heartbeatTimer = Timer.periodic( Duration(seconds: _config.heartbeatIntervalSeconds), (_) => _sendHeartbeat(), ); debugPrint('💓 [Heartbeat] Started'); } /// 停止心跳 void _stopHeartbeat() { _heartbeatTimer?.cancel(); _heartbeatTimer = null; _isRunning = false; debugPrint('💓 [Heartbeat] Stopped (count: $_heartbeatCount)'); } /// 发送心跳 Future _sendHeartbeat() async { // 检查是否需要登录 if (_config.requiresAuth && (getUserId?.call() == null)) { debugPrint('💓 [Heartbeat] Skipped: user not logged in'); return; } try { final response = await _dio.post( '/api/v1/presence/heartbeat', data: { 'installId': getInstallId?.call() ?? '', 'appVersion': getAppVersion?.call() ?? '', 'clientTs': DateTime.now().millisecondsSinceEpoch ~/ 1000, }, options: Options( headers: getAuthHeaders?.call(), ), ); if (response.statusCode == 200) { _lastHeartbeatAt = DateTime.now(); _heartbeatCount++; debugPrint('💓 [Heartbeat] Sent #$_heartbeatCount'); } } catch (e) { // 心跳失败不重试,等待下一个周期 debugPrint('💓 [Heartbeat] Failed: $e'); } } /// 手动触发心跳(用于测试) @visibleForTesting Future forceHeartbeat() async { await _sendHeartbeat(); } } ``` ### 2.7 核心服务(整合版) **文件: `lib/core/telemetry/telemetry_service.dart`** ```dart import 'dart:async'; import 'dart:math'; import 'package:uuid/uuid.dart'; import 'package:flutter/material.dart'; import 'models/telemetry_event.dart'; import 'models/device_context.dart'; import 'models/telemetry_config.dart'; import 'collectors/device_info_collector.dart'; import 'storage/telemetry_storage.dart'; import 'uploader/telemetry_uploader.dart'; import 'session/session_manager.dart'; import 'session/session_events.dart'; import 'presence/heartbeat_service.dart'; import 'presence/presence_config.dart'; class TelemetryService { static TelemetryService? _instance; TelemetryService._(); factory TelemetryService() { _instance ??= TelemetryService._(); return _instance!; } final _storage = TelemetryStorage(); late TelemetryUploader _uploader; DeviceContext? _deviceContext; /// 🆕 安装ID(设备唯一标识,首次安装生成,用于未登录用户的DAU去重) late String _installId; String get installId => _installId; /// 用户ID(登录后设置) String? _userId; String? get userId => _userId; /// API 基础地址 late String _apiBaseUrl; bool _isInitialized = false; bool get isInitialized => _isInitialized; /// 🆕 会话管理器 late SessionManager _sessionManager; /// 🆕 心跳服务 late HeartbeatService _heartbeatService; /// 初始化(在main.dart中调用) Future initialize({ required String apiBaseUrl, required BuildContext context, String? userId, Duration configSyncInterval = const Duration(hours: 1), PresenceConfig? presenceConfig, }) async { if (_isInitialized) return; _apiBaseUrl = apiBaseUrl; // 1. 初始化存储 await _storage.init(); // 2. 🆕 初始化或获取 installId await _initInstallId(); // 3. 加载用户选择 await TelemetryConfig().loadUserOptIn(); // 4. 同步远程配置(首次) await TelemetryConfig().syncFromRemote(apiBaseUrl); // 5. 收集设备信息 _deviceContext = await DeviceInfoCollector().collect(context); await _storage.saveDeviceContext(_deviceContext!.toJson()); // 6. 设置用户ID _userId = userId; // 7. 初始化上传器 _uploader = TelemetryUploader( apiBaseUrl: apiBaseUrl, storage: _storage, getAuthHeaders: _getAuthHeaders, ); // 8. 启动定时上传(如果启用) if (TelemetryConfig().globalEnabled) { _uploader.startPeriodicUpload(); } // 9. 定期同步配置 Timer.periodic(configSyncInterval, (_) async { await TelemetryConfig().syncFromRemote(apiBaseUrl); // 根据最新配置调整上传器状态 if (TelemetryConfig().globalEnabled) { _uploader.startPeriodicUpload(); } else { _uploader.stopPeriodicUpload(); } // 🆕 更新心跳配置 if (TelemetryConfig().presenceConfig != null) { _heartbeatService.updateConfig(TelemetryConfig().presenceConfig!); } }); // 10. 🆕 初始化会话管理器 _sessionManager = SessionManager(); _sessionManager.initialize(this); // 11. 🆕 初始化心跳服务 _heartbeatService = HeartbeatService(); _heartbeatService.initialize( apiBaseUrl: apiBaseUrl, config: presenceConfig ?? TelemetryConfig().presenceConfig, getInstallId: () => _installId, getUserId: () => _userId, getAppVersion: () => _deviceContext?.appVersion ?? 'unknown', getAuthHeaders: _getAuthHeaders, ); _isInitialized = true; debugPrint('📊 TelemetryService initialized'); debugPrint(' InstallId: $_installId'); debugPrint(' UserId: $_userId'); } /// 🆕 初始化 installId Future _initInstallId() async { final storedId = _storage.getInstallId(); if (storedId != null) { _installId = storedId; } else { _installId = const Uuid().v4(); await _storage.saveInstallId(_installId); } debugPrint('📊 [Telemetry] Install ID: $_installId'); } /// 🆕 获取认证头 Map _getAuthHeaders() { // TODO: 从你的 AuthService 获取 token // final token = AuthService.instance.accessToken; // if (token != null) { // return {'Authorization': 'Bearer $token'}; // } return {}; } /// 记录事件(核心方法) void logEvent( String eventName, { EventType type = EventType.userAction, EventLevel level = EventLevel.info, Map? properties, }) { if (!_isInitialized) { debugPrint('⚠️ TelemetryService not initialized, event ignored'); return; } // 检查配置:是否应该记录 if (!TelemetryConfig().shouldLog(type, eventName)) { return; // 🚫 配置禁止记录 } // 采样判断(错误和崩溃不采样) if (_needsSampling(type)) { if (Random().nextDouble() > TelemetryConfig().samplingRate) { return; // 🚫 未被采样 } } final event = TelemetryEvent( eventId: const Uuid().v4(), type: type, level: level, name: eventName, properties: properties, timestamp: DateTime.now(), userId: _userId, sessionId: _sessionManager.currentSessionId, installId: _installId, deviceContextId: _deviceContext!.androidId, ); _storage.enqueueEvent(event); // 检查是否需要立即上传 _uploader.uploadIfNeeded(); } /// 判断是否需要采样 bool _needsSampling(EventType type) { // 错误、崩溃、会话事件 100% 上报,不采样 return type != EventType.error && type != EventType.crash && type != EventType.session; } /// 记录页面访问 void logPageView(String pageName, {Map? extra}) { logEvent( 'page_view', type: EventType.pageView, properties: {'page': pageName, ...?extra}, ); } /// 记录用户行为 void logUserAction(String action, {Map? properties}) { logEvent( action, type: EventType.userAction, properties: properties, ); } /// 记录错误 void logError( String errorMessage, { Object? error, StackTrace? stackTrace, Map? extra, }) { logEvent( 'error_occurred', type: EventType.error, level: EventLevel.error, properties: { 'message': errorMessage, 'error': error?.toString(), 'stack_trace': stackTrace?.toString(), ...?extra, }, ); } /// 记录API调用 void logApiCall({ required String url, required String method, required int statusCode, required int durationMs, String? error, }) { logEvent( 'api_call', type: EventType.apiCall, level: error != null ? EventLevel.error : EventLevel.info, properties: { 'url': url, 'method': method, 'status_code': statusCode, 'duration_ms': durationMs, 'error': error, }, ); } /// 记录性能指标 void logPerformance( String metricName, { required int durationMs, Map? extra, }) { logEvent( metricName, type: EventType.performance, properties: {'duration_ms': durationMs, ...?extra}, ); } /// 设置用户ID(登录后调用) void setUserId(String? userId) { _userId = userId; debugPrint('📊 [Telemetry] User ID set: $userId'); } /// 清除用户ID(退出登录时) void clearUserId() { _userId = null; debugPrint('📊 [Telemetry] User ID cleared'); } /// 设置用户是否同意数据收集 Future setUserOptIn(bool optIn) async { await TelemetryConfig().setUserOptIn(optIn); if (!optIn) { // 用户拒绝,停止上传并清空队列 _uploader.stopPeriodicUpload(); await _storage.clearEventQueue(); debugPrint('📊 Telemetry disabled by user'); } else { // 用户同意,重新启动 if (TelemetryConfig().globalEnabled) { _uploader.startPeriodicUpload(); } debugPrint('📊 Telemetry enabled by user'); } } // ========== 🆕 会话和在线状态相关方法 ========== /// 获取当前会话 ID String? get currentSessionId => _sessionManager.currentSessionId; /// 获取会话时长(秒) int get sessionDurationSeconds => _sessionManager.sessionDurationSeconds; /// 心跳是否运行中 bool get isHeartbeatRunning => _heartbeatService.isRunning; /// 心跳计数 int get heartbeatCount => _heartbeatService.heartbeatCount; /// 更新心跳配置 void updatePresenceConfig(PresenceConfig config) { _heartbeatService.updateConfig(config); } /// App退出前调用 Future dispose() async { _sessionManager.dispose(); _heartbeatService.dispose(); await _uploader.forceUploadAll(); debugPrint('📊 TelemetryService disposed'); } } ``` ### 2.8 远程配置模型(扩展版) **文件: `lib/core/telemetry/models/telemetry_config.dart`** ```dart import 'package:dio/dio.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'telemetry_event.dart'; import '../presence/presence_config.dart'; class TelemetryConfig { // 全局开关 bool globalEnabled = true; // 分类型开关 bool errorReportEnabled = true; // 错误上报 bool performanceEnabled = true; // 性能监控 bool userActionEnabled = true; // 用户行为 bool pageViewEnabled = true; // 页面访问 bool sessionEnabled = true; // 🆕 会话事件(DAU相关) // 采样配置 double samplingRate = 0.1; // 10% 采样率 // 事件黑名单 List disabledEvents = []; // 配置版本 String configVersion = '1.0.0'; // 用户是否同意(可选,用于隐私合规) bool userOptIn = true; // 🆕 心跳/在线状态配置 PresenceConfig? presenceConfig; static final TelemetryConfig _instance = TelemetryConfig._(); TelemetryConfig._(); factory TelemetryConfig() => _instance; /// 从后端同步配置 Future syncFromRemote(String apiBaseUrl) async { try { final dio = Dio(); final response = await dio.get('$apiBaseUrl/api/telemetry/config'); final data = response.data; globalEnabled = data['global_enabled'] ?? true; errorReportEnabled = data['error_report_enabled'] ?? true; performanceEnabled = data['performance_enabled'] ?? true; userActionEnabled = data['user_action_enabled'] ?? true; pageViewEnabled = data['page_view_enabled'] ?? true; sessionEnabled = data['session_enabled'] ?? true; samplingRate = (data['sampling_rate'] ?? 0.1).toDouble(); disabledEvents = List.from(data['disabled_events'] ?? []); configVersion = data['version'] ?? '1.0.0'; // 🆕 解析心跳配置 if (data['presence_config'] != null) { presenceConfig = PresenceConfig.fromJson(data['presence_config']); } // 缓存到本地 await _saveToLocal(); print('📊 Telemetry config synced (v$configVersion)'); print(' Global: $globalEnabled, Sampling: ${(samplingRate * 100).toInt()}%'); print(' Presence: ${presenceConfig?.enabled ?? true}'); } catch (e) { print('⚠️ Failed to sync telemetry config: $e'); // 失败时加载本地缓存 await _loadFromLocal(); } } /// 保存到本地 Future _saveToLocal() async { final prefs = await SharedPreferences.getInstance(); await prefs.setBool('telemetry_global_enabled', globalEnabled); await prefs.setBool('telemetry_error_enabled', errorReportEnabled); await prefs.setBool('telemetry_performance_enabled', performanceEnabled); await prefs.setBool('telemetry_user_action_enabled', userActionEnabled); await prefs.setBool('telemetry_page_view_enabled', pageViewEnabled); await prefs.setBool('telemetry_session_enabled', sessionEnabled); await prefs.setDouble('telemetry_sampling_rate', samplingRate); await prefs.setStringList('telemetry_disabled_events', disabledEvents); await prefs.setString('telemetry_config_version', configVersion); } /// 从本地加载 Future _loadFromLocal() async { final prefs = await SharedPreferences.getInstance(); globalEnabled = prefs.getBool('telemetry_global_enabled') ?? true; errorReportEnabled = prefs.getBool('telemetry_error_enabled') ?? true; performanceEnabled = prefs.getBool('telemetry_performance_enabled') ?? true; userActionEnabled = prefs.getBool('telemetry_user_action_enabled') ?? true; pageViewEnabled = prefs.getBool('telemetry_page_view_enabled') ?? true; sessionEnabled = prefs.getBool('telemetry_session_enabled') ?? true; samplingRate = prefs.getDouble('telemetry_sampling_rate') ?? 0.1; disabledEvents = prefs.getStringList('telemetry_disabled_events') ?? []; configVersion = prefs.getString('telemetry_config_version') ?? '1.0.0'; } /// 判断是否应该记录该事件 bool shouldLog(EventType type, String eventName) { // 1. 全局开关 if (!globalEnabled) return false; // 2. 用户未同意 if (!userOptIn) return false; // 3. 事件黑名单 if (disabledEvents.contains(eventName)) return false; // 4. 分类型判断 switch (type) { case EventType.error: case EventType.crash: return errorReportEnabled; case EventType.performance: return performanceEnabled; case EventType.userAction: return userActionEnabled; case EventType.pageView: return pageViewEnabled; case EventType.apiCall: return performanceEnabled; // API调用归入性能监控 case EventType.session: return sessionEnabled; // 🆕 会话事件 case EventType.presence: return presenceConfig?.enabled ?? true; // 🆕 在线状态 } } /// 设置用户是否同意 Future setUserOptIn(bool optIn) async { userOptIn = optIn; final prefs = await SharedPreferences.getInstance(); await prefs.setBool('telemetry_user_opt_in', optIn); print('📊 User opt-in: $optIn'); } /// 加载用户选择 Future loadUserOptIn() async { final prefs = await SharedPreferences.getInstance(); userOptIn = prefs.getBool('telemetry_user_opt_in') ?? true; } } ``` --- ## 三、使用示例 ### 3.1 初始化 **文件: `lib/main.dart`** ```dart import 'package:flutter/material.dart'; import 'core/telemetry/telemetry_service.dart'; import 'core/telemetry/presence/presence_config.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); // 设置全局错误捕获 FlutterError.onError = (details) { TelemetryService().logError( 'Flutter error', error: details.exception, stackTrace: details.stack, extra: {'context': details.context?.toString()}, ); }; runApp(const MyApp()); } class MyApp extends StatefulWidget { const MyApp({Key? key}) : super(key: key); @override State createState() => _MyAppState(); } class _MyAppState extends State { @override void initState() { super.initState(); _initTelemetry(); } Future _initTelemetry() async { // 延迟到第一帧后初始化 WidgetsBinding.instance.addPostFrameCallback((_) async { await TelemetryService().initialize( apiBaseUrl: 'https://your-backend.com', context: context, userId: null, // 登录后再设置 configSyncInterval: const Duration(hours: 1), presenceConfig: const PresenceConfig( heartbeatIntervalSeconds: 60, requiresAuth: true, enabled: true, ), ); }); } @override Widget build(BuildContext context) { return MaterialApp( title: 'Your App', home: const HomePage(), ); } } ``` ### 3.2 登录后设置用户 ID ```dart // 登录成功后 void onLoginSuccess(User user) { TelemetryService().setUserId(user.id.toString()); } // 登出后清除 void onLogout() { TelemetryService().clearUserId(); } ``` ### 3.3 业务代码中使用 ```dart // 页面访问埋点 class PlantingPage extends StatefulWidget { @override State createState() => _PlantingPageState(); } class _PlantingPageState extends State { @override void initState() { super.initState(); // 记录页面访问 TelemetryService().logPageView( 'planting_page', extra: {'source': 'home_banner'}, ); } void _onConfirmPlanting() { // 记录用户行为 TelemetryService().logUserAction( 'planting_confirm_clicked', properties: { 'tree_id': 123, 'price_usdt': 800, }, ); // 执行种植逻辑... } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('种植页面')), body: Center( child: ElevatedButton( onPressed: _onConfirmPlanting, child: Text('确认种植'), ), ), ); } } ``` ### 3.4 网络请求拦截 ```dart // 封装Dio拦截器 class TelemetryInterceptor extends Interceptor { @override void onRequest(RequestOptions options, RequestInterceptorHandler handler) { options.extra['start_time'] = DateTime.now().millisecondsSinceEpoch; super.onRequest(options, handler); } @override void onResponse(Response response, ResponseInterceptorHandler handler) { final startTime = response.requestOptions.extra['start_time'] as int; final duration = DateTime.now().millisecondsSinceEpoch - startTime; TelemetryService().logApiCall( url: response.requestOptions.path, method: response.requestOptions.method, statusCode: response.statusCode ?? 0, durationMs: duration, ); super.onResponse(response, handler); } @override void onError(DioException err, ErrorInterceptorHandler handler) { final startTime = err.requestOptions.extra['start_time'] as int?; final duration = startTime != null ? DateTime.now().millisecondsSinceEpoch - startTime : 0; TelemetryService().logApiCall( url: err.requestOptions.path, method: err.requestOptions.method, statusCode: err.response?.statusCode ?? 0, durationMs: duration, error: err.message, ); super.onError(err, handler); } } // 在初始化Dio时添加 final dio = Dio()..interceptors.add(TelemetryInterceptor()); ``` ### 3.5 性能监控 ```dart // 封装性能监控工具 class PerformanceTracker { final Stopwatch _stopwatch = Stopwatch(); final String _metricName; PerformanceTracker(this._metricName); void start() { _stopwatch.start(); } void stop() { _stopwatch.stop(); TelemetryService().logPerformance( _metricName, durationMs: _stopwatch.elapsedMilliseconds, ); } } // 使用示例 Future loadData() async { final tracker = PerformanceTracker('home_page_load'); tracker.start(); try { // 加载数据... await Future.delayed(Duration(seconds: 2)); } finally { tracker.stop(); } } ``` --- ## 四、后端接口设计 ### 4.1 事件批量上报接口 ```http POST /api/v1/analytics/events Content-Type: application/json Authorization: Bearer (可选) { "events": [ { "eventId": "uuid-1", "type": "session", "level": "info", "name": "app_session_start", "properties": { "session_id": "sess-uuid-xxx" }, "timestamp": "2024-01-15T10:30:00Z", "userId": "12345", "sessionId": "sess-uuid-xxx", "installId": "install-uuid-xxx", "deviceContextId": "android_id_abc" }, { "eventId": "uuid-2", "type": "userAction", "level": "info", "name": "planting_confirm_clicked", "properties": { "tree_id": 123, "price_usdt": 800 }, "timestamp": "2024-01-15T10:35:00Z", "userId": "12345", "sessionId": "sess-uuid-xxx", "installId": "install-uuid-xxx", "deviceContextId": "android_id_abc" } ] } Response: { "accepted": 2, "failed": 0 } ``` ### 4.2 心跳接口(在线统计专用) ```http POST /api/v1/presence/heartbeat Content-Type: application/json Authorization: Bearer (必填,仅登录用户) { "installId": "install-uuid-xxx", "appVersion": "1.2.0", "clientTs": 1705312200 } Response: { "ok": true, "serverTs": 1705312201 } ``` ### 4.3 配置接口(扩展版) ```http GET /api/telemetry/config Response: { "global_enabled": true, "error_report_enabled": true, "performance_enabled": true, "user_action_enabled": true, "page_view_enabled": true, "session_enabled": true, "sampling_rate": 0.1, "disabled_events": [], "version": "1.0.3", "presence_config": { "enabled": true, "heartbeat_interval_seconds": 60, "requires_auth": true } } ``` ### 4.4 后端存储建议 **数据库选型:** - **设备上下文表**: PostgreSQL/MySQL (结构化数据) - **事件日志**: PostgreSQL (初期) → ClickHouse/ElasticSearch (规模扩大后) - **在线状态**: Redis ZSET (实时统计) - **聚合统计**: Redis/ClickHouse (实时分析) **表结构示例:** ```sql -- 事件日志表 (用于 DAU 计算) CREATE TABLE analytics_event_log ( id BIGSERIAL PRIMARY KEY, user_id BIGINT, -- 可为空(未登录用户) install_id VARCHAR(64) NOT NULL, -- 安装ID event_name VARCHAR(64) NOT NULL, event_time TIMESTAMPTZ NOT NULL DEFAULT NOW(), properties JSONB, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX idx_event_log_event_time ON analytics_event_log (event_time); CREATE INDEX idx_event_log_event_name ON analytics_event_log (event_name); CREATE INDEX idx_event_log_event_name_time ON analytics_event_log (event_name, event_time); -- DAU 统计表 CREATE TABLE analytics_daily_active_users ( day DATE PRIMARY KEY, dau_count INTEGER NOT NULL, dau_by_province JSONB, dau_by_city JSONB, calculated_at TIMESTAMPTZ NOT NULL, version INTEGER NOT NULL DEFAULT 1 ); -- 在线人数快照表 CREATE TABLE analytics_online_snapshots ( id BIGSERIAL PRIMARY KEY, ts TIMESTAMPTZ NOT NULL UNIQUE, online_count INTEGER NOT NULL, window_seconds INTEGER NOT NULL DEFAULT 180 ); CREATE INDEX idx_online_snapshots_ts ON analytics_online_snapshots (ts DESC); ``` **Redis 数据结构:** ``` # 在线状态 ZSET Key: presence:online_users Type: ZSET Member: user_id (string) Score: last_heartbeat_timestamp (unix seconds) # 操作示例 ZADD presence:online_users 1705312200 "12345" ZCOUNT presence:online_users (1705312020) +inf # 3分钟内在线人数 ZREMRANGEBYSCORE presence:online_users -inf (1705225800) # 清理24小时前数据 ``` --- ## 五、高级功能 ### 5.1 采样策略 ```dart class TelemetryService { // ... 前面的代码 /// 判断是否需要采样 bool _needsSampling(EventType type) { // 错误、崩溃、会话事件 100% 上报,不采样 // 会话事件必须 100% 上报才能保证 DAU 准确性 return type != EventType.error && type != EventType.crash && type != EventType.session; } } ``` ### 5.2 远程配置开关(三级控制) **配置策略建议:** **阶段1:初期上线(保守策略)** ```json { "global_enabled": true, "error_report_enabled": true, "performance_enabled": false, "user_action_enabled": false, "page_view_enabled": false, "session_enabled": true, "sampling_rate": 0.05, "presence_config": { "enabled": true, "heartbeat_interval_seconds": 60, "requires_auth": true } } ``` **阶段2:稳定运行1周后** ```json { "global_enabled": true, "error_report_enabled": true, "performance_enabled": true, "user_action_enabled": false, "page_view_enabled": true, "session_enabled": true, "sampling_rate": 0.1, "presence_config": { "enabled": true, "heartbeat_interval_seconds": 60, "requires_auth": true } } ``` **阶段3:完全开放** ```json { "global_enabled": true, "error_report_enabled": true, "performance_enabled": true, "user_action_enabled": true, "page_view_enabled": true, "session_enabled": true, "sampling_rate": 0.2, "presence_config": { "enabled": true, "heartbeat_interval_seconds": 60, "requires_auth": true } } ``` ### 5.3 隐私保护 ```dart // 数据脱敏 class PrivacyHelper { /// 移除敏感字段 static Map sanitize(Map data) { final sanitized = Map.from(data); // 移除可能包含隐私的字段 sanitized.remove('phone'); sanitized.remove('email'); sanitized.remove('real_name'); // URL参数脱敏 if (sanitized['url'] != null) { sanitized['url'] = _sanitizeUrl(sanitized['url']); } return sanitized; } static String _sanitizeUrl(String url) { final uri = Uri.parse(url); // 只保留path,移除query参数 return uri.replace(query: '').toString(); } } ``` ### 5.4 用户设置界面 **文件: `lib/pages/settings/telemetry_settings_page.dart`** ```dart import 'package:flutter/material.dart'; import '../../core/telemetry/telemetry_service.dart'; import '../../core/telemetry/models/telemetry_config.dart'; class TelemetrySettingsPage extends StatefulWidget { const TelemetrySettingsPage({Key? key}) : super(key: key); @override State createState() => _TelemetrySettingsPageState(); } class _TelemetrySettingsPageState extends State { bool _userOptIn = true; @override void initState() { super.initState(); _loadUserPreference(); } Future _loadUserPreference() async { await TelemetryConfig().loadUserOptIn(); setState(() { _userOptIn = TelemetryConfig().userOptIn; }); } Future _toggleOptIn(bool value) async { await TelemetryService().setUserOptIn(value); setState(() { _userOptIn = value; }); ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(value ? '已开启数据收集' : '已关闭数据收集'), duration: const Duration(seconds: 2), ), ); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: const Text('隐私与数据')), body: ListView( children: [ SwitchListTile( title: const Text('帮助改进应用'), subtitle: const Text( '发送匿名使用数据、崩溃报告和性能指标\n' '我们重视您的隐私,不会收集任何个人信息', ), value: _userOptIn, onChanged: _toggleOptIn, ), const Divider(), ListTile( title: const Text('我们收集什么?'), subtitle: const Text('点击查看详情'), trailing: const Icon(Icons.arrow_forward_ios, size: 16), onTap: () { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('数据收集说明'), content: const SingleChildScrollView( child: Text( '我们收集以下匿名信息以改进应用:\n\n' '• 设备信息:品牌、型号、系统版本\n' '• 应用崩溃和错误日志\n' '• 页面加载时间等性能数据\n' '• 匿名的功能使用统计\n' '• 日活跃用户统计(DAU)\n' '• 在线状态统计\n\n' '我们不会收集:\n' '• 手机号、IMEI等设备识别码\n' '• 您的个人信息和聊天内容\n' '• 位置信息\n', ), ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('知道了'), ), ], ), ); }, ), ], ), ); } } ``` --- ## 六、调试与验证 ### 6.1 本地测试 ```dart // 在debug模式下打印上报内容 if (kDebugMode) { print('📊 Event queued: ${event.name}'); print(' Properties: ${event.properties}'); print(' Queue size: ${storage.getQueueSize()}'); } ``` ### 6.2 调试页面 ```dart class TelemetryDebugPage extends StatelessWidget { @override Widget build(BuildContext context) { final telemetry = TelemetryService(); return Scaffold( appBar: AppBar(title: const Text('Telemetry Debug')), body: ListView( padding: const EdgeInsets.all(16), children: [ _InfoTile('Install ID', telemetry.installId), _InfoTile('User ID', telemetry.userId ?? 'Not logged in'), _InfoTile('Session ID', telemetry.currentSessionId ?? 'No session'), _InfoTile('Session Duration', '${telemetry.sessionDurationSeconds}s'), _InfoTile('Heartbeat Running', telemetry.isHeartbeatRunning.toString()), _InfoTile('Heartbeat Count', telemetry.heartbeatCount.toString()), const Divider(), ElevatedButton( onPressed: () { telemetry.logUserAction('debug_test_event'); }, child: const Text('Send Test Event'), ), const SizedBox(height: 8), ElevatedButton( onPressed: () { telemetry.logPageView('debug_page'); }, child: const Text('Send Page View'), ), ], ), ); } } class _InfoTile extends StatelessWidget { final String label; final String value; const _InfoTile(this.label, this.value); @override Widget build(BuildContext context) { return ListTile( title: Text(label), subtitle: Text(value), ); } } ``` --- ## 七、实施步骤 ### Step 1: 添加依赖 ```bash # 在 pubspec.yaml 添加依赖后执行 flutter pub get ``` ### Step 2: 生成JSON序列化代码 ```bash flutter pub run build_runner build --delete-conflicting-outputs ``` ### Step 3: 创建核心文件 按照上面的架构,依次创建: 1. `models/` 下的数据模型 2. `collectors/` 下的收集器 3. `storage/` 下的存储 4. `uploader/` 下的上报器 5. `session/` 下的会话管理模块 🆕 6. `presence/` 下的心跳模块 🆕 7. `telemetry_service.dart` 核心服务 ### Step 4: 初始化服务 在 `main.dart` 中初始化 TelemetryService ### Step 5: 业务埋点 在需要的地方调用: - `logPageView()` - 页面访问 - `logUserAction()` - 用户行为 - `logError()` - 错误日志 - `logPerformance()` - 性能指标 - **会话事件自动触发** 🆕 - **心跳自动发送** 🆕 ### Step 6: 搭建后端 - 创建 `/api/v1/analytics/events` 接口接收事件数据 - 创建 `/api/v1/presence/heartbeat` 接口接收心跳 🆕 - 配置 Redis 存储在线状态 🆕 - 设计数据库存储结构 - 实现 DAU 计算定时任务 🆕 - 搭建分析Dashboard --- ## 八、最佳实践 ### ✅ DO 1. **分级记录**: debug/info/warn/error,生产环境只上报 warn 以上 2. **批量上传**: 积累一定数量或时间后再上传,减少网络请求 3. **失败重试**: 上传失败保留本地,下次启动继续尝试 4. **性能优先**: 上报逻辑不能阻塞主线程 5. **隐私保护**: 绝不收集IMEI、手机号等敏感信息 6. **开关控制**: 提供远程开关,随时可以关闭上报 7. **会话事件100%上报**: DAU 统计需要完整数据 🆕 8. **心跳仅前台发送**: 后台停止心跳,节省电量 🆕 ### ❌ DON'T 1. **不要同步上报**: 会阻塞UI 2. **不要频繁上传**: 每个事件立即上传会耗电和流量 3. **不要无限缓存**: 设置队列上限,防止占用过多存储 4. **不要记录密码**: 任何密码、token 都不能出现在日志中 5. **不要忽略用户意愿**: 如果用户关闭数据收集,必须停止 6. **不要对会话事件采样**: 会导致 DAU 不准确 🆕 7. **不要在后台发送心跳**: 浪费电量和流量 🆕 --- ## 九、扩展方向 1. **集成Firebase Crashlytics**: 自动捕获Native崩溃 2. **集成Sentry**: 更强大的错误追踪和Session Replay 3. **添加ANR检测**: 监控Android主线程卡顿 4. **添加内存监控**: 记录内存使用峰值 5. **添加启动耗时**: 分析cold start和warm start时间 6. **用户行为漏斗**: 分析关键路径的转化率 7. **A/B测试集成**: 配合实验平台做功能验证 8. **WAU/MAU 统计**: 周活、月活用户统计 🆕 9. **用户留存分析**: 基于 DAU 数据分析留存率 🆕 10. **在线峰值监控**: 记录和告警在线人数峰值 🆕 --- ## 十、DAU & 在线统计数据流转 ### 10.1 DAU 统计流程 ``` 1. App 启动/切回前台 ↓ 2. SessionManager 检测到 AppLifecycleState.resumed ↓ 3. 生成新 sessionId,调用 logEvent('app_session_start') ↓ 4. 事件进入本地队列 (TelemetryStorage) ↓ 5. TelemetryUploader 批量上报到 POST /api/v1/analytics/events ↓ 6. 后端写入 analytics_event_log 表 ↓ 7. 定时任务按 COALESCE(user_id, install_id) 去重计算 DAU ↓ 8. 结果写入 analytics_daily_active_users 表 ``` ### 10.2 在线人数统计流程 ``` 1. SessionManager 触发 onSessionStart 回调 ↓ 2. HeartbeatService 启动定时器 (60秒) ↓ 3. 每 60 秒调用 POST /api/v1/presence/heartbeat ↓ 4. 后端更新 Redis ZSET: ZADD presence:online_users ↓ 5. 查询在线人数: ZCOUNT presence:online_users (now-180) +inf ↓ 6. App 进入后台 → SessionManager 触发 onSessionEnd ↓ 7. HeartbeatService 停止定时器 ``` --- ## 十一、总结 这套方案的核心思路: ``` 物理设备 → 收集器 → 本地队列 → 批量上报 → 后端分析 ↓ 会话管理 → app_session_start → DAU 统计 ↓ 心跳服务 → presence_heartbeat → 在线人数统计 ``` **优势:** - ✅ 离线可用,不依赖网络 - ✅ 批量上传,节省流量 - ✅ 结构化存储,便于后续分析 - ✅ 可扩展性强,易于添加新指标 - ✅ 对主线程无影响 - ✅ **DAU 统计精准,支持登录/未登录用户** 🆕 - ✅ **在线人数实时统计,3分钟窗口判定** 🆕 - ✅ **心跳机制对电量影响可控** 🆕 **典型场景:** - 📱 兼容性问题: "为什么这个型号的手机总是崩溃?" - 🐛 Bug定位: "这个错误在什么场景下触发的?" - ⚡ 性能优化: "哪些页面加载慢?哪些接口超时?" - 📊 用户行为: "用户在哪个环节流失最多?" - 📈 **运营分析: "今天有多少活跃用户?当前多少人在线?"** 🆕 - 🎯 **容量规划: "在线峰值是多少?需要扩容吗?"** 🆕 --- **下一步行动:** 1. 复制代码到项目中 2. 根据实际需求调整配置 3. 搭建后端接收接口(事件 + 心跳) 4. 配置 Redis 存储在线状态 5. 在关键页面添加埋点 6. 实现 DAU 计算定时任务 7. 搭建 Grafana 看板展示 DAU 和在线人数 8. 观察数据,持续迭代优化 有问题随时找我! 🚀