added firebase analytics

This commit is contained in:
Thibault Deckers 2020-10-27 14:25:57 +09:00
parent 41e7d889b6
commit 4a5919a979
9 changed files with 87 additions and 25 deletions

View file

@ -8,6 +8,8 @@ import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/common/routes.dart'; import 'package:aves/widgets/common/routes.dart';
import 'package:aves/widgets/home_page.dart'; import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/welcome_page.dart'; import 'package:aves/widgets/welcome_page.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_analytics/observer.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -41,7 +43,9 @@ class AvesApp extends StatefulWidget {
class _AvesAppState extends State<AvesApp> { class _AvesAppState extends State<AvesApp> {
Future<void> _appSetup; Future<void> _appSetup;
final NavigatorObserver _routeTracker = CrashlyticsRouteTracker(); // observers are not registered when using the same list object with different items
// the list itself needs to be reassigned
List<NavigatorObserver> _navigatorObservers = [];
final _newIntentChannel = EventChannel('deckers.thibault/aves/intent'); final _newIntentChannel = EventChannel('deckers.thibault/aves/intent');
final _navigatorKey = GlobalKey<NavigatorState>(); final _navigatorKey = GlobalKey<NavigatorState>();
@ -93,11 +97,12 @@ class _AvesAppState extends State<AvesApp> {
Future<void> _setup() async { Future<void> _setup() async {
await Firebase.initializeApp().then((app) { await Firebase.initializeApp().then((app) {
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterError; final crashlytics = FirebaseCrashlytics.instance;
FirebaseCrashlytics.instance.setCustomKey('locales', window.locales.join(', ')); FlutterError.onError = crashlytics.recordFlutterError;
crashlytics.setCustomKey('locales', window.locales.join(', '));
final now = DateTime.now(); final now = DateTime.now();
FirebaseCrashlytics.instance.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})'); crashlytics.setCustomKey('timezone', '${now.timeZoneName} (${now.timeZoneOffset})');
FirebaseCrashlytics.instance.setCustomKey( crashlytics.setCustomKey(
'build_mode', 'build_mode',
kReleaseMode kReleaseMode
? 'release' ? 'release'
@ -106,7 +111,11 @@ class _AvesAppState extends State<AvesApp> {
: 'debug'); : 'debug');
}); });
await settings.init(); await settings.init();
await settings.initCrashlytics(); await settings.initFirebase();
_navigatorObservers = [
FirebaseAnalyticsObserver(analytics: FirebaseAnalytics()),
CrashlyticsRouteTracker(),
];
} }
void _onNewIntent(Map intentData) { void _onNewIntent(Map intentData) {
@ -126,28 +135,20 @@ class _AvesAppState extends State<AvesApp> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
// place the settings provider above `MaterialApp` // place the settings provider above `MaterialApp`
// so it can be used during navigation transitions // so it can be used during navigation transitions
final home = FutureBuilder<void>(
future: _appSetup,
builder: (context, snapshot) {
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) {
return getFirstPage();
}
return Scaffold(
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(),
);
},
);
return SettingsProvider( return SettingsProvider(
child: OverlaySupport( child: OverlaySupport(
child: FutureBuilder<void>( child: FutureBuilder<void>(
future: _appSetup, future: _appSetup,
builder: (context, snapshot) { builder: (context, snapshot) {
final home = (!snapshot.hasError && snapshot.connectionState == ConnectionState.done)
? getFirstPage()
: Scaffold(
body: snapshot.hasError ? _buildError(snapshot.error) : SizedBox.shrink(),
);
return MaterialApp( return MaterialApp(
navigatorKey: _navigatorKey, navigatorKey: _navigatorKey,
home: home, home: home,
navigatorObservers: [ navigatorObservers: _navigatorObservers,
if (!snapshot.hasError && snapshot.connectionState == ConnectionState.done) _routeTracker,
],
title: 'Aves', title: 'Aves',
darkTheme: darkTheme, darkTheme: darkTheme,
themeMode: ThemeMode.dark, themeMode: ThemeMode.dark,

View file

@ -3,6 +3,7 @@ import 'package:aves/model/settings/coordinate_format.dart';
import 'package:aves/model/settings/home_page.dart'; import 'package:aves/model/settings/home_page.dart';
import 'package:aves/model/settings/screen_on.dart'; import 'package:aves/model/settings/screen_on.dart';
import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:aves/widgets/fullscreen/info/location_section.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
@ -57,9 +58,14 @@ class Settings extends ChangeNotifier {
// Crashlytics initialization is separated from the main settings initialization // Crashlytics initialization is separated from the main settings initialization
// to allow settings customization without Firebase context (e.g. before a Flutter Driver test) // to allow settings customization without Firebase context (e.g. before a Flutter Driver test)
Future<void> initCrashlytics() async { Future<void> initFirebase() async {
await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled); await Firebase.app().setAutomaticDataCollectionEnabled(isCrashlyticsEnabled);
await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled); await FirebaseCrashlytics.instance.setCrashlyticsCollectionEnabled(isCrashlyticsEnabled);
await FirebaseAnalytics().setAnalyticsCollectionEnabled(isCrashlyticsEnabled);
// enable analytics debug mode:
// # %ANDROID_SDK%/platform-tools/adb shell setprop debug.firebase.analytics.app deckers.thibault.aves.debug
// disable analytics debug mode:
// # %ANDROID_SDK%/platform-tools/adb shell setprop debug.firebase.analytics.app .none.
} }
Future<void> reset() { Future<void> reset() {
@ -76,7 +82,7 @@ class Settings extends ChangeNotifier {
set isCrashlyticsEnabled(bool newValue) { set isCrashlyticsEnabled(bool newValue) {
setAndNotify(isCrashlyticsEnabledKey, newValue); setAndNotify(isCrashlyticsEnabledKey, newValue);
unawaited(initCrashlytics()); unawaited(initFirebase());
} }
bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true); bool get mustBackTwiceToExit => getBoolOrDefault(mustBackTwiceToExitKey, true);

View file

@ -13,6 +13,7 @@ import 'package:aves/utils/file_utils.dart';
import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/common/icons.dart';
import 'package:aves/widgets/fullscreen/info/info_page.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:firebase_core/firebase_core.dart'; import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart'; import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
@ -109,6 +110,21 @@ class AppDebugPageState extends State<AppDebugPage> {
), ),
], ],
), ),
Row(
children: [
Expanded(
child: Text('Analytics'),
),
SizedBox(width: 8),
ElevatedButton(
onPressed: () => FirebaseAnalytics().logEvent(
name: 'debug_test',
parameters: {'time': DateTime.now().toIso8601String()},
),
child: Text('Send event'),
),
],
),
Text('Firebase data collection: ${Firebase.app().isAutomaticDataCollectionEnabled ? 'enabled' : 'disabled'}'), Text('Firebase data collection: ${Firebase.app().isAutomaticDataCollectionEnabled ? 'enabled' : 'disabled'}'),
Text('Crashlytics collection: ${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled ? 'enabled' : 'disabled'}'), Text('Crashlytics collection: ${FirebaseCrashlytics.instance.isCrashlyticsCollectionEnabled ? 'enabled' : 'disabled'}'),
Divider(), Divider(),

View file

@ -6,8 +6,10 @@ import 'package:aves/model/metadata_db.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_source.dart'; import 'package:aves/model/source/collection_source.dart';
import 'package:aves/services/image_file_service.dart'; import 'package:aves/services/image_file_service.dart';
import 'package:firebase_analytics/firebase_analytics.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_native_timezone/flutter_native_timezone.dart'; import 'package:flutter_native_timezone/flutter_native_timezone.dart';
import 'package:pedantic/pedantic.dart';
class MediaStoreSource extends CollectionSource { class MediaStoreSource extends CollectionSource {
Future<void> init() async { Future<void> init() async {
@ -68,16 +70,31 @@ class MediaStoreSource extends CollectionSource {
onDone: () async { onDone: () async {
addPendingEntries(); addPendingEntries();
debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}'); debugPrint('$runtimeType refresh loaded ${allNewEntries.length} new entries, elapsed=${stopwatch.elapsed}');
await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries await metadataDb.saveEntries(allNewEntries); // 700ms for 5500 entries
updateAlbums(); updateAlbums();
final analytics = FirebaseAnalytics();
unawaited(analytics.setUserProperty(name: 'local_item_count', value: (ceilBy(rawEntries.length, 3)).toString()));
unawaited(analytics.setUserProperty(name: 'album_count', value: (ceilBy(sortedAlbums.length, 1)).toString()));
stateNotifier.value = SourceState.cataloguing; stateNotifier.value = SourceState.cataloguing;
await catalogEntries(); await catalogEntries();
unawaited(analytics.setUserProperty(name: 'tag_count', value: (ceilBy(sortedTags.length, 1)).toString()));
stateNotifier.value = SourceState.locating; stateNotifier.value = SourceState.locating;
await locateEntries(); await locateEntries();
unawaited(analytics.setUserProperty(name: 'country_count', value: (ceilBy(sortedCountries.length, 1)).toString()));
stateNotifier.value = SourceState.ready; stateNotifier.value = SourceState.ready;
debugPrint('$runtimeType refresh done, elapsed=${stopwatch.elapsed}'); debugPrint('$runtimeType refresh done, elapsed=${stopwatch.elapsed}');
}, },
onError: (error) => debugPrint('$runtimeType stream error=$error'), onError: (error) => debugPrint('$runtimeType stream error=$error'),
); );
} }
// e.g. x=12345, precision=3 should return 13000
int ceilBy(num x, int precision) {
final factor = pow(10, precision);
return (x / factor).ceil() * factor;
}
} }

View file

@ -46,7 +46,7 @@ abstract class ChipSetActionDelegate {
options: { options: {
ChipSortFactor.date: 'By date', ChipSortFactor.date: 'By date',
ChipSortFactor.name: 'By name', ChipSortFactor.name: 'By name',
ChipSortFactor.count: 'By entry count', ChipSortFactor.count: 'By item count',
}, },
title: 'Sort', title: 'Sort',
), ),

View file

@ -115,7 +115,7 @@ class SettingsPage extends StatelessWidget {
SwitchListTile( SwitchListTile(
value: settings.isCrashlyticsEnabled, value: settings.isCrashlyticsEnabled,
onChanged: (v) => settings.isCrashlyticsEnabled = v, onChanged: (v) => settings.isCrashlyticsEnabled = v,
title: Text('Allow anonymous crash reporting'), title: Text('Allow anonymous analytics and crash reporting'),
), ),
GrantedDirectories(), GrantedDirectories(),
], ],

View file

@ -99,7 +99,7 @@ class _WelcomePageState extends State<WelcomePage> {
LabeledCheckbox( LabeledCheckbox(
value: settings.isCrashlyticsEnabled, value: settings.isCrashlyticsEnabled,
onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v), onChanged: (v) => setState(() => settings.isCrashlyticsEnabled = v),
text: 'Allow anonymous crash reporting', text: 'Allow anonymous analytics and crash reporting',
), ),
LabeledCheckbox( LabeledCheckbox(
key: Key('agree-checkbox'), key: Key('agree-checkbox'),

View file

@ -201,6 +201,27 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "7.3.2" version: "7.3.2"
firebase_analytics:
dependency: "direct main"
description:
name: firebase_analytics
url: "https://pub.dartlang.org"
source: hosted
version: "6.0.2"
firebase_analytics_platform_interface:
dependency: transitive
description:
name: firebase_analytics_platform_interface
url: "https://pub.dartlang.org"
source: hosted
version: "1.0.3"
firebase_analytics_web:
dependency: transitive
description:
name: firebase_analytics_web
url: "https://pub.dartlang.org"
source: hosted
version: "0.1.1"
firebase_core: firebase_core:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -49,6 +49,7 @@ dependencies:
git: git:
url: git://github.com/deckerst/expansion_tile_card.git url: git://github.com/deckerst/expansion_tile_card.git
firebase_core: firebase_core:
firebase_analytics:
firebase_crashlytics: firebase_crashlytics:
flushbar: flushbar:
flutter_ijkplayer: flutter_ijkplayer: