Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,8 @@
## Observability

- firebase_core (Firebase initialization)
- firebase_crashlytics (opt-in crash reporting)
- firebase_crashlytics (crash reporting, enabled by default)
- firebase_performance (performance monitoring, enabled by default)

## Internationalization

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ class CrashlyticsPreferenceStorage {

bool read() {
try {
return _prefs.getBool(_key) ?? false;
return _prefs.getBool(_key) ?? true;
} catch (error, stack) {
debugPrint('Failed to read crashlytics preference: $error\n$stack');
return false;
return true;
}
Comment thread
using-system marked this conversation as resolved.
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';

class PerformancePreferenceStorage {
PerformancePreferenceStorage(this._prefs);

static const _key = 'observability_performance_enabled';

final SharedPreferences _prefs;

bool read() {
try {
return _prefs.getBool(_key) ?? true;
} catch (error, stack) {
debugPrint('Failed to read performance preference: $error\n$stack');
return true;
}
}

Future<void> write(bool enabled) async {
final didWrite = await _prefs.setBool(_key, enabled);
if (!didWrite) {
throw Exception('Failed to persist performance preference.');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class CrashlyticsToggleTile extends ConsumerWidget {
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final crashlyticsAsync = ref.watch(crashlyticsPreferenceProvider);
final enabled = crashlyticsAsync.valueOrNull ?? false;
final enabled = crashlyticsAsync.valueOrNull ?? true;

return SwitchListTile(
secondary: const Icon(Icons.bug_report_outlined),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';

import 'crashlytics_toggle_tile.dart';
import 'performance_toggle_tile.dart';

class ObservabilitySection extends StatelessWidget {
const ObservabilitySection({super.key});
Expand All @@ -12,6 +13,8 @@ class ObservabilitySection extends StatelessWidget {
children: [
CrashlyticsToggleTile(),
Divider(height: 1),
PerformanceToggleTile(),
Divider(height: 1),
],
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import 'package:cookmate/l10n/app_localizations.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../providers.dart';

class PerformanceToggleTile extends ConsumerWidget {
const PerformanceToggleTile({super.key});

@override
Widget build(BuildContext context, WidgetRef ref) {
final l10n = AppLocalizations.of(context);
final performanceAsync = ref.watch(performancePreferenceProvider);
final enabled = performanceAsync.valueOrNull ?? true;

return SwitchListTile(
secondary: const Icon(Icons.speed),
title: Text(l10n.settingsPerformanceTitle),
subtitle: Text(l10n.settingsPerformanceDescription),
value: enabled,
onChanged: performanceAsync.isLoading ? null : (value) async {
try {
await ref
.read(performancePreferenceProvider.notifier)
.setPreference(value);
} catch (error, stack) {
debugPrint('Failed to change performance: $error\n$stack');
if (context.mounted) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content:
Text(l10n.settingsPerformanceChangeFailureSnackbar),
),
);
}
}
},
);
}
}
40 changes: 40 additions & 0 deletions lib/features/observability/providers.dart
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';

import '../../core/shared_preferences_provider.dart';
import 'data/crashlytics_preference_storage.dart';
import 'data/performance_preference_storage.dart';

final crashlyticsPreferenceStorageProvider =
FutureProvider<CrashlyticsPreferenceStorage>((ref) async {
Expand Down Expand Up @@ -42,3 +44,41 @@ final crashlyticsPreferenceProvider =
AsyncNotifierProvider<CrashlyticsPreferenceNotifier, bool>(
CrashlyticsPreferenceNotifier.new,
);

final performancePreferenceStorageProvider =
FutureProvider<PerformancePreferenceStorage>((ref) async {
final prefs = await ref.watch(sharedPreferencesProvider.future);
return PerformancePreferenceStorage(prefs);
});

class PerformancePreferenceNotifier extends AsyncNotifier<bool> {
@override
Future<bool> build() async {
final storage =
await ref.watch(performancePreferenceStorageProvider.future);
return storage.read();
}

Future<void> setPreference(bool enabled) async {
final storage =
await ref.read(performancePreferenceStorageProvider.future);
state = const AsyncValue<bool>.loading().copyWithPrevious(state);
try {
await storage.write(enabled);
if (Firebase.apps.isNotEmpty) {
await FirebasePerformance.instance
.setPerformanceCollectionEnabled(enabled);
}
state = AsyncValue.data(enabled);
} catch (error, stack) {
state =
AsyncValue<bool>.error(error, stack).copyWithPrevious(state);
rethrow;
}
}
}

final performancePreferenceProvider =
AsyncNotifierProvider<PerformancePreferenceNotifier, bool>(
PerformancePreferenceNotifier.new,
);
6 changes: 5 additions & 1 deletion lib/l10n/app_de.arb
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,9 @@
"settingsSectionObservability": "Beobachtbarkeit",
"settingsCrashlyticsTitle": "Absturzbericht",
"settingsCrashlyticsDescription": "Anonyme Absturzberichte senden, um die App zu verbessern",
"settingsCrashlyticsChangeFailureSnackbar": "Absturzbericht-Einstellung konnte nicht geändert werden. Bitte versuchen Sie es erneut."
"settingsCrashlyticsChangeFailureSnackbar": "Absturzbericht-Einstellung konnte nicht geändert werden. Bitte versuchen Sie es erneut.",

"settingsPerformanceTitle": "Leistungsüberwachung",
"settingsPerformanceDescription": "Anonyme Leistungsdaten sammeln, um die App zu verbessern",
"settingsPerformanceChangeFailureSnackbar": "Leistungsüberwachung-Einstellung konnte nicht geändert werden. Bitte versuchen Sie es erneut."
}
11 changes: 10 additions & 1 deletion lib/l10n/app_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -452,5 +452,14 @@
"@settingsCrashlyticsDescription": { "description": "Subtitle explaining what crash reporting does." },

"settingsCrashlyticsChangeFailureSnackbar": "Couldn't change crash reporting setting. Please try again.",
"@settingsCrashlyticsChangeFailureSnackbar": { "description": "Shown when persisting the crash reporting toggle fails." }
"@settingsCrashlyticsChangeFailureSnackbar": { "description": "Shown when persisting the crash reporting toggle fails." },

"settingsPerformanceTitle": "Performance monitoring",
"@settingsPerformanceTitle": { "description": "Title of the performance monitoring toggle tile." },

"settingsPerformanceDescription": "Collect anonymous performance data to help improve the app",
"@settingsPerformanceDescription": { "description": "Subtitle explaining what performance monitoring does." },

"settingsPerformanceChangeFailureSnackbar": "Couldn't change performance monitoring setting. Please try again.",
"@settingsPerformanceChangeFailureSnackbar": { "description": "Shown when persisting the performance monitoring toggle fails." }
}
6 changes: 5 additions & 1 deletion lib/l10n/app_es.arb
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,9 @@
"settingsSectionObservability": "Observabilidad",
"settingsCrashlyticsTitle": "Informe de fallos",
"settingsCrashlyticsDescription": "Enviar informes de fallos anónimos para mejorar la aplicación",
"settingsCrashlyticsChangeFailureSnackbar": "No se pudo cambiar la configuración de informe de fallos. Inténtelo de nuevo."
"settingsCrashlyticsChangeFailureSnackbar": "No se pudo cambiar la configuración de informe de fallos. Inténtelo de nuevo.",

"settingsPerformanceTitle": "Monitoreo de rendimiento",
"settingsPerformanceDescription": "Recopilar datos de rendimiento anónimos para mejorar la aplicación",
"settingsPerformanceChangeFailureSnackbar": "No se pudo cambiar la configuración de monitoreo de rendimiento. Inténtelo de nuevo."
}
6 changes: 5 additions & 1 deletion lib/l10n/app_fr.arb
Original file line number Diff line number Diff line change
Expand Up @@ -147,5 +147,9 @@
"settingsSectionObservability": "Observabilité",
"settingsCrashlyticsTitle": "Rapport de plantage",
"settingsCrashlyticsDescription": "Envoyer des rapports de plantage anonymes pour améliorer l'application",
"settingsCrashlyticsChangeFailureSnackbar": "Impossible de modifier le paramètre de rapport de plantage. Veuillez réessayer."
"settingsCrashlyticsChangeFailureSnackbar": "Impossible de modifier le paramètre de rapport de plantage. Veuillez réessayer.",

"settingsPerformanceTitle": "Rapport de performance",
"settingsPerformanceDescription": "Collecter des données de performance anonymes pour améliorer l'application",
"settingsPerformanceChangeFailureSnackbar": "Impossible de modifier le paramètre de performance. Veuillez réessayer."
}
11 changes: 11 additions & 0 deletions lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import 'dart:async';

import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
import 'package:firebase_performance/firebase_performance.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_gemma/flutter_gemma.dart';
Expand All @@ -10,6 +11,7 @@ import 'package:shared_preferences/shared_preferences.dart';

import 'app.dart';
import 'features/observability/data/crashlytics_preference_storage.dart';
import 'features/observability/data/performance_preference_storage.dart';

void main() {
runZonedGuarded(
Expand All @@ -31,6 +33,15 @@ void main() {

FlutterError.onError =
FirebaseCrashlytics.instance.recordFlutterFatalError;

try {
final performanceEnabled =
PerformancePreferenceStorage(prefs).read();
await FirebasePerformance.instance
.setPerformanceCollectionEnabled(performanceEnabled);
} catch (e, stack) {
debugPrint('Firebase Performance init skipped: $e\n$stack');
}
} catch (e, stack) {
debugPrint('Firebase init skipped: $e\n$stack');
}
Expand Down
24 changes: 24 additions & 0 deletions pubspec.lock
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,30 @@ packages:
url: "https://pub.dev"
source: hosted
version: "3.8.10"
firebase_performance:
dependency: "direct main"
description:
name: firebase_performance
sha256: b6948a02cb06d5da36e9f91f4a96b39745ab7565942a8134b7692ea6fc4ac595
url: "https://pub.dev"
source: hosted
version: "0.10.1+10"
firebase_performance_platform_interface:
dependency: transitive
description:
name: firebase_performance_platform_interface
sha256: "836b219dc99c59ee5b854ba4f6432781f40ae75b31ea112f3b8b2ffce8a01cd4"
url: "https://pub.dev"
source: hosted
version: "0.1.5+10"
firebase_performance_web:
dependency: transitive
description:
name: firebase_performance_web
sha256: "7b8cdc99f8c7ceafd6c3f51a3a661aef96bc9c5948af78d35bc9660941ed5b2f"
url: "https://pub.dev"
source: hosted
version: "0.1.7+16"
fixnum:
dependency: transitive
description:
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ dependencies:
http: ^1.4.0
firebase_core: ^3.13.0
firebase_crashlytics: ^4.3.5
firebase_performance: ^0.10.0+12

dev_dependencies:
flutter_test:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ void main() {
storage = CrashlyticsPreferenceStorage(prefs);
});

test('read returns false when nothing is stored', () {
expect(storage.read(), false);
test('read returns true when nothing is stored', () {
expect(storage.read(), true);
});

test('write then read returns the written value', () async {
Expand All @@ -28,12 +28,12 @@ void main() {
expect(storage.read(), false);
});

test('read returns false when stored value is corrupted', () async {
test('read returns true when stored value is corrupted', () async {
SharedPreferences.setMockInitialValues(<String, Object>{
'observability_crashlytics_enabled': 'not_a_bool',
});
final prefs = await SharedPreferences.getInstance();
final s = CrashlyticsPreferenceStorage(prefs);
expect(s.read(), false);
expect(s.read(), true);
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import 'package:cookmate/features/observability/data/performance_preference_storage.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:shared_preferences/shared_preferences.dart';

void main() {
TestWidgetsFlutterBinding.ensureInitialized();

late PerformancePreferenceStorage storage;

setUp(() async {
SharedPreferences.setMockInitialValues(<String, Object>{});
final prefs = await SharedPreferences.getInstance();
storage = PerformancePreferenceStorage(prefs);
});

test('read returns true when nothing is stored', () {
expect(storage.read(), true);
});

test('write then read returns the written value', () async {
await storage.write(false);
expect(storage.read(), false);
});

test('write overwrites a previous value', () async {
await storage.write(false);
await storage.write(true);
expect(storage.read(), true);
});

test('read returns true when stored value is corrupted', () async {
SharedPreferences.setMockInitialValues(<String, Object>{
'observability_performance_enabled': 'not_a_bool',
});
final prefs = await SharedPreferences.getInstance();
final s = PerformancePreferenceStorage(prefs);
expect(s.read(), true);
});
}
Loading