在完成 Day 18 的錯誤追蹤系統後,我們已經能快速發現和修復問題。不過,除了知道「哪裡出錯」,我們更想了解「使用者如何使用 App」。今天我們要為 Crew Up! 建立使用者行為分析系統,就像 App 的數據儀表板,幫助我們用真實數據做出產品決策。
開發功能時,我們常常會有很多假設:「使用者應該會這樣操作」、「這個功能很重要」。不過一旦 App 上線後,實際情況往往和想像不同。從我們的開發經驗來看,數據分析能幫助我們更了解真實的使用情況。
📊 我們想了解的問題
🎯 Analytics 的價值
# pubspec.yaml
dependencies:
firebase_analytics: ^12.0.2 # Firebase Analytics SDK
firebase_core: ^4.1.0 # Firebase 核心
flutter_riverpod: ^2.6.1 # 狀態管理
shared_preferences: ^2.2.2 # 本地儲存(同意狀態)
💡 關於 Firebase 專案設定,請參考 Day 13 - Firebase 專案設定
// lib/app/core/services/analytics_service.dart
/// Firebase Analytics 服務
class AnalyticsService {
final FirebaseAnalytics _analytics;
AnalyticsService({FirebaseAnalytics? analytics})
: _analytics = analytics ?? FirebaseAnalytics.instance;
/// 記錄自定義事件
Future<void> logEvent({
required String eventName,
Map<String, Object?>? parameters,
}) async {
try {
await _analytics.logEvent(
name: eventName,
parameters: parameters?.map((key, value) => MapEntry(key, value ?? '')),
);
} on Exception catch (e) {
developer.log('Analytics error: $e', name: 'Analytics');
}
}
/// 記錄頁面瀏覽
Future<void> logScreenView({
required String screenName,
String? screenClass,
}) async {
await logEvent(
eventName: 'screen_view',
parameters: {
'screen_name': screenName,
if (screenClass != null) 'screen_class': screenClass,
},
);
}
/// 記錄活動相關事件
Future<void> logActivityEvent({
required String action,
required String activityId,
String? category,
}) async {
await logEvent(
eventName: 'activity_$action',
parameters: {
'activity_id': activityId,
if (category != null) 'category': category,
},
);
}
/// 記錄搜索事件
Future<void> logSearch({
required String searchTerm,
String? category,
}) async {
await logEvent(
eventName: 'search',
parameters: {
'search_term': searchTerm,
if (category != null) 'category': category,
},
);
}
/// 設置用戶屬性
Future<void> setUserProperty({
required String name,
required String value,
}) async {
try {
await _analytics.setUserProperty(name: name, value: value);
} on Exception catch (e) {
developer.log('Analytics error: $e', name: 'Analytics');
}
}
/// 設置用戶ID
Future<void> setUserId(String userId) async {
try {
await _analytics.setUserId(id: userId);
} on Exception catch (e) {
developer.log('Analytics error: $e', name: 'Analytics');
}
}
/// 記錄登入
Future<void> logLogin({String? method}) async {
await _analytics.logLogin(loginMethod: method);
}
/// 記錄註冊
Future<void> logSignUp({String? method}) async {
await _analytics.logSignUp(signUpMethod: method ?? 'unknown');
}
/// 啟用/停用分析收集
Future<void> setAnalyticsCollectionEnabled(bool enabled) async {
try {
await _analytics.setAnalyticsCollectionEnabled(enabled);
} on Exception catch (e) {
developer.log('Analytics error: $e', name: 'Analytics');
}
}
}
@riverpod
AnalyticsService analyticsService(Ref ref) => AnalyticsService();
我們使用 Clean Architecture 的 Domain 層來管理 Analytics 同意的業務邏輯:
核心概念:
AnalyticsConsentRepository
:定義同意管理的抽象介面ManageAnalyticsConsentUseCase
:封裝業務規則和邏輯AnalyticsConsent
:代表同意狀態的實體主要功能:
AnalyticsConsentManager
實作 Domain 層的介面,負責實際的數據儲存和管理:
核心功能:
SharedPreferences
儲存同意狀態和時間戳AnalyticsService
整合,控制數據收集的啟用/停用關鍵實作細節:
AnalyticsConsentDialogProvider:管理同意對話框的顯示狀態,避免重複顯示。
核心邏輯:
AnalyticsConsentDialog:向使用者清楚說明數據收集的目的和範圍,提供明確的同意選項。
設計要點:
AnalyticsSettingsTile:在設定頁面提供開關,讓使用者可以隨時更改同意狀態。
AnalyticsConsentInitializer:在 App 啟動時自動處理 Analytics 同意流程。
關鍵功能:
在 main.dart
中,我們確保 Firebase 初始化時預設停用 Analytics 收集,等待使用者同意:
// lib/main.dart
// (imports omitted)
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize Firebase (idempotent)
await ensureFirebaseInitialized();
// GDPR Compliance: app_open event will be logged after user consent
// is obtained in AnalyticsConsentInitializer
developer.log(
'📊 Analytics: Waiting for user consent before logging events',
name: 'Analytics',
);
runApp(UncontrolledProviderScope(container: container, child: const App()));
}
在 app.dart
中,我們用 AnalyticsConsentInitializer
包裹整個 App:
// lib/app.dart
// (imports omitted)
class App extends ConsumerWidget {
const App({super.key});
@override
Widget build(BuildContext context, WidgetRef ref) =>
AnalyticsConsentInitializer(
child: MaterialApp.router(
// App 配置...
),
);
}
基於 Firebase 的限制和我們專案中的實務經驗:
✅ 我們推薦的作法
// 使用小寫字母和底線
'activity_created'
'join_activity_completed'
// 使用預定義常量
{
'activity_id': 'activity_123',
'category': 'sports',
}
❌ 我們建議避免的作法
// 包含空格和大寫
'Activity Created'
'Join Activity'
// 使用保留前綴
'firebase_custom_event'
'google_my_event'
在我們實作過程中需要注意的限制:
✅ 我們推薦的作法
// 不記錄個人身分資訊
await analytics.logEvent(
eventName: 'user_signed_up',
parameters: {
'method': 'email',
'timestamp': DateTime.now().toIso8601String(),
},
);
❌ 我們建議避免的作法
// 不要記錄 PII
await analytics.logEvent(
eventName: 'user_signed_up',
parameters: {
'email': 'user@example.com', // ❌ 不要記錄
'phone': '+886912345678', // ❌ 不要記錄
'real_name': '王小明', // ❌ 不要記錄
},
);
在 Firebase Console 驗證
Firebase Analytics 提供 DebugView 功能,可以即時查看事件:
啟用 DebugView 的步驟:
DebugView 的使用技巧:
事件沒有顯示在 Console
使用者屬性沒有更新
setUserId
)今天我們為 Crew Up! 建立了完整的 Analytics 系統:
✅ 已完成的功能
🎯 技術亮點
從實際開發中,我們學到:
明天(Day 20),我們將探討 Firebase Performance Monitoring,學習如何追蹤 App 的效能指標,發現並優化效能瓶頸,讓 Crew Up! 在各種裝置上都能流暢運行。
期待與您在 Day 20 相見!