feat(analytics): wire Firebase Analytics into app startup

flutterfire configure registered the iOS/Android apps under project
block-seasons and generated firebase_options.dart + native config. main()
now initializes Firebase and routes analytics through FirebaseAnalyticsBackend
in release builds (console logger in debug, so dev traffic never pollutes
GA4). Firebase init is guarded — failure falls back to the debug logger
rather than blocking startup.

firebase.json keeps the existing Hosting config and gains the FlutterFire
platform section. Client config files are committed (they ship in the binary;
Firebase security is enforced by rules, not config secrecy).

flutter analyze clean, all 161 tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-06-13 12:10:03 +09:00
parent e3fb5959c5
commit 41b0180b44
8 changed files with 160 additions and 10 deletions
+3
View File
@@ -1,5 +1,8 @@
plugins {
id("com.android.application")
// START: FlutterFire Configuration
id("com.google.gms.google-services")
// END: FlutterFire Configuration
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
+29
View File
@@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "190209969950",
"project_id": "block-seasons",
"storage_bucket": "block-seasons.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:190209969950:android:e08dc30877b1821b44c30f",
"android_client_info": {
"package_name": "com.airkjw.blockseasons"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCd_3Tw5IxlO6ysp3XMQ9tBsOM1yMYl2MU"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}
+3
View File
@@ -20,6 +20,9 @@ pluginManagement {
plugins {
id("dev.flutter.flutter-plugin-loader") version "1.0.0"
id("com.android.application") version "8.9.1" apply false
// START: FlutterFire Configuration
id("com.google.gms.google-services") version("4.4.4") apply false
// END: FlutterFire Configuration
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
}
+1 -10
View File
@@ -1,10 +1 @@
{
"hosting": {
"public": "deploy",
"ignore": [
"firebase.json",
"**/.*",
"**/node_modules/**"
]
}
}
{"hosting":{"public":"deploy","ignore":["firebase.json","**/.*","**/node_modules/**"]},"flutter":{"platforms":{"android":{"default":{"projectId":"block-seasons","appId":"1:190209969950:android:e08dc30877b1821b44c30f","fileOutput":"android/app/google-services.json"}},"ios":{"default":{"projectId":"block-seasons","appId":"1:190209969950:ios:f9d4578ec86f92c844c30f","uploadDebugSymbols":false,"fileOutput":"ios/Runner/GoogleService-Info.plist"}},"dart":{"lib/firebase_options.dart":{"projectId":"block-seasons","configurations":{"android":"1:190209969950:android:e08dc30877b1821b44c30f","ios":"1:190209969950:ios:f9d4578ec86f92c844c30f"}}}}}}
+4
View File
@@ -16,6 +16,7 @@
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
A1624C49AABB61D3BB6EBA00 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7F021B835BC4E346AE82B4C9 /* Pods_RunnerTests.framework */; };
D444497F007A61A9102D174D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 92F5ACA56D636C056F52DDE6 /* Pods_Runner.framework */; };
E746073DDE80B82D8D3C9659 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
@@ -47,6 +48,7 @@
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = "<group>"; };
55914DA7E8E89CB02E73C3F5 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
6BD9A45428DD4E519FC38754 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
@@ -129,6 +131,7 @@
331C8082294A63A400263BE5 /* RunnerTests */,
2EECBA43D42E2853F949CCFC /* Pods */,
9CFCC4FE458D4EC11DAF9E88 /* Frameworks */,
43C03408D7DF6E3F6C4EC9C7 /* GoogleService-Info.plist */,
);
sourceTree = "<group>";
};
@@ -264,6 +267,7 @@
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */,
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */,
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */,
E746073DDE80B82D8D3C9659 /* GoogleService-Info.plist in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
+30
View File
@@ -0,0 +1,30 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>API_KEY</key>
<string>AIzaSyAIl2LM2fDrr7OH70K7uJnAwKwXMzPCBMI</string>
<key>GCM_SENDER_ID</key>
<string>190209969950</string>
<key>PLIST_VERSION</key>
<string>1</string>
<key>BUNDLE_ID</key>
<string>com.airkjw.blockseasons</string>
<key>PROJECT_ID</key>
<string>block-seasons</string>
<key>STORAGE_BUCKET</key>
<string>block-seasons.firebasestorage.app</string>
<key>IS_ADS_ENABLED</key>
<false></false>
<key>IS_ANALYTICS_ENABLED</key>
<false></false>
<key>IS_APPINVITE_ENABLED</key>
<true></true>
<key>IS_GCM_ENABLED</key>
<true></true>
<key>IS_SIGNIN_ENABLED</key>
<true></true>
<key>GOOGLE_APP_ID</key>
<string>1:190209969950:ios:f9d4578ec86f92c844c30f</string>
</dict>
</plist>
+68
View File
@@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyCd_3Tw5IxlO6ysp3XMQ9tBsOM1yMYl2MU',
appId: '1:190209969950:android:e08dc30877b1821b44c30f',
messagingSenderId: '190209969950',
projectId: 'block-seasons',
storageBucket: 'block-seasons.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyAIl2LM2fDrr7OH70K7uJnAwKwXMzPCBMI',
appId: '1:190209969950:ios:f9d4578ec86f92c844c30f',
messagingSenderId: '190209969950',
projectId: 'block-seasons',
storageBucket: 'block-seasons.firebasestorage.app',
iosBundleId: 'com.airkjw.blockseasons',
);
}
+22
View File
@@ -1,5 +1,7 @@
import 'dart:io';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
@@ -8,6 +10,9 @@ import 'app.dart';
import 'data/content_repository.dart';
import 'data/remote/content_downloader.dart';
import 'data/save_repository.dart';
import 'firebase_options.dart';
import 'services/analytics_service.dart';
import 'services/firebase_analytics_backend.dart';
import 'state/providers.dart';
/// Remote content origin. Swap per environment with
@@ -22,6 +27,22 @@ Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
final saveRepository = await SaveRepository.open();
// Analytics: real GA4 traffic flows only from release builds so development
// never pollutes production. If Firebase init fails (e.g. missing native
// config), fall back to the console logger rather than blocking startup.
AnalyticsService analytics;
try {
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
analytics = AnalyticsService(
kReleaseMode ? FirebaseAnalyticsBackend() : DebugAnalyticsBackend(),
);
} catch (e) {
debugPrint('Firebase init failed, analytics disabled: $e');
analytics = AnalyticsService(DebugAnalyticsBackend());
}
ContentRepository contentRepository;
try {
final support = await getApplicationSupportDirectory();
@@ -42,6 +63,7 @@ Future<void> main() async {
overrides: [
saveRepositoryProvider.overrideWithValue(saveRepository),
contentRepositoryProvider.overrideWithValue(contentRepository),
analyticsProvider.overrideWithValue(analytics),
],
child: const BlockSeasonsApp(),
));