diff --git a/frontend/mining-app/lib/core/router/app_router.dart b/frontend/mining-app/lib/core/router/app_router.dart index 0e04acf7..a1069baf 100644 --- a/frontend/mining-app/lib/core/router/app_router.dart +++ b/frontend/mining-app/lib/core/router/app_router.dart @@ -31,6 +31,7 @@ import '../../presentation/pages/profile/notification_inbox_page.dart'; import '../../presentation/widgets/main_shell.dart'; import '../../presentation/providers/user_providers.dart'; import 'routes.dart'; +import 'telemetry_route_observer.dart'; /// 不需要登录就能访问的公开路由 const _publicRoutes = { @@ -72,6 +73,7 @@ final appRouterProvider = Provider((ref) { return GoRouter( initialLocation: Routes.splash, refreshListenable: authNotifier, + observers: [TelemetryRouteObserver()], redirect: (context, state) { // 使用 authNotifier 中缓存的状态,而不是 ref.read final isLoggedIn = authNotifier.isLoggedIn; diff --git a/frontend/mining-app/lib/core/router/telemetry_route_observer.dart b/frontend/mining-app/lib/core/router/telemetry_route_observer.dart new file mode 100644 index 00000000..a165a9eb --- /dev/null +++ b/frontend/mining-app/lib/core/router/telemetry_route_observer.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import '../telemetry/telemetry_service.dart'; + +/// 路由遥测观察者 +/// +/// 挂载到 GoRouter.observers,自动追踪所有页面访问。 +/// 无需在每个页面手动调用 logPageView()。 +class TelemetryRouteObserver extends NavigatorObserver { + static const _pageNames = { + '/': 'splash', + '/login': 'login', + '/register': 'register', + '/forgot-password': 'forgot_password', + '/change-password': 'change_password', + '/trade-password': 'trade_password', + '/contribution': 'contribution', + '/trading': 'trading', + '/asset': 'asset', + '/profile': 'profile', + '/edit-profile': 'edit_profile', + '/mining-records': 'mining_records', + '/batch-mining-records': 'batch_mining_records', + '/contribution-records': 'contribution_records', + '/planting-records': 'planting_records', + '/send-shares': 'send_shares', + '/receive-shares': 'receive_shares', + '/c2c-market': 'c2c_market', + '/c2c-publish': 'c2c_publish', + '/c2c-order-detail': 'c2c_order_detail', + '/my-team': 'my_team', + '/trading-records': 'trading_records', + '/transfer-records': 'transfer_records', + '/p2p-transfer-records': 'p2p_transfer_records', + '/notifications': 'notifications', + '/help-center': 'help_center', + '/about': 'about', + }; + + void _log(Route? route) { + if (route == null) return; + final name = route.settings.name; + if (name == null || name.isEmpty) return; + final path = name.split('?').first; + final pageName = _pageNames[path] ?? path.replaceAll('/', '').replaceAll('-', '_'); + TelemetryService().logPageView(pageName); + } + + @override + void didPush(Route route, Route? previousRoute) { + super.didPush(route, previousRoute); + _log(route); + } + + @override + void didReplace({Route? newRoute, Route? oldRoute}) { + super.didReplace(newRoute: newRoute, oldRoute: oldRoute); + _log(newRoute); + } +} diff --git a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart index 4c69047f..0dd933ad 100644 --- a/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart +++ b/frontend/mining-app/lib/presentation/pages/trading/trading_page.dart @@ -19,6 +19,7 @@ import '../../widgets/shimmer_loading.dart'; import '../../widgets/kline_chart/kline_chart_widget.dart'; import '../../widgets/trade_password_dialog.dart'; import '../../../data/models/capability_model.dart'; +import '../../../core/telemetry/telemetry_service.dart'; class TradingPage extends ConsumerStatefulWidget { const TradingPage({super.key}); @@ -1540,6 +1541,13 @@ class _TradingPageState extends ConsumerState { .sellShares(_priceController.text, _quantityController.text); } + final action = isBuy ? 'buy_shares' : 'sell_shares'; + TelemetryService().logUserAction(action, properties: { + 'success': success, + 'price': _priceController.text, + 'quantity': _quantityController.text, + }); + if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -1960,6 +1968,11 @@ class _TransferBottomSheetState extends ConsumerState<_TransferBottomSheet> { success = await ref.read(tradingNotifierProvider.notifier).transferOut(amount); } + TelemetryService().logUserAction('transfer_shares', properties: { + 'direction': _direction == 0 ? 'in' : 'out', + 'amount': amount, + 'success': success, + }); if (mounted) { if (success) { Navigator.pop(context); diff --git a/frontend/mining-app/lib/presentation/providers/user_providers.dart b/frontend/mining-app/lib/presentation/providers/user_providers.dart index 842fe962..8f1be6d8 100644 --- a/frontend/mining-app/lib/presentation/providers/user_providers.dart +++ b/frontend/mining-app/lib/presentation/providers/user_providers.dart @@ -178,9 +178,11 @@ class UserNotifier extends StateNotifier { TelemetryService().setUserId(result.user.accountSequence); TelemetryService().setAccessToken(result.accessToken); TelemetryService().resumeAfterLogin(); + TelemetryService().logUserAction('login_success', properties: {'method': 'password'}); // 登录后获取详细信息 await fetchProfile(); } catch (e) { + TelemetryService().logUserAction('login_failed', properties: {'method': 'password', 'error': e.toString()}); state = state.copyWith(isLoading: false, error: e.toString()); rethrow; } @@ -204,9 +206,11 @@ class UserNotifier extends StateNotifier { TelemetryService().setUserId(result.user.accountSequence); TelemetryService().setAccessToken(result.accessToken); TelemetryService().resumeAfterLogin(); + TelemetryService().logUserAction('login_success', properties: {'method': 'sms'}); // 登录后获取详细信息 await fetchProfile(); } catch (e) { + TelemetryService().logUserAction('login_failed', properties: {'method': 'sms', 'error': e.toString()}); state = state.copyWith(isLoading: false, error: e.toString()); rethrow; } @@ -218,6 +222,7 @@ class UserNotifier extends StateNotifier { await _authDataSource.logout(state.refreshToken!); } catch (_) {} } + TelemetryService().logUserAction('logout'); await TelemetryService().pauseForLogout(); TelemetryService().setAccessToken(null); await _clearStorage();