feat(sf_tracking): consent-aware crashlytics wrapper
This commit is contained in:
@@ -1,9 +1,9 @@
|
||||
import 'package:firebase_analytics/firebase_analytics.dart';
|
||||
import 'package:firebase_core/firebase_core.dart';
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:firebase_performance/firebase_performance.dart';
|
||||
import 'package:firebase_remote_config/firebase_remote_config.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
import '../config/env/environment_enum.dart';
|
||||
import '../firebase_options_dev.dart' as dev_options;
|
||||
@@ -25,9 +25,12 @@ Future<void> setupFirebase(EnvironmentEnum env) async {
|
||||
await Firebase.initializeApp(options: options);
|
||||
|
||||
// Report crashes in ALL builds (debug + release) so we catch issues during testing too.
|
||||
FlutterError.onError = FirebaseCrashlytics.instance.recordFlutterFatalError;
|
||||
// TODO(gdpr): wire `enabled` to real consent once the fix in backlog lands.
|
||||
final crashlytics = FirebaseCrashlyticsService(enabled: true);
|
||||
FlutterError.onError = (details) =>
|
||||
crashlytics.recordFlutterError(details, fatal: true);
|
||||
PlatformDispatcher.instance.onError = (error, stack) {
|
||||
FirebaseCrashlytics.instance.recordError(error, stack, fatal: true);
|
||||
crashlytics.recordError(error, stack, fatal: true);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
/// SaveFamily tracking & analytics package.
|
||||
library;
|
||||
|
||||
export 'src/clients/crashlytics_service.dart';
|
||||
export 'src/clients/debug_tracking_client.dart';
|
||||
export 'src/clients/firebase_tracking_client.dart';
|
||||
export 'src/mixins/account_tracking.dart';
|
||||
@@ -16,6 +17,7 @@ export 'src/mixins/settings_tracking.dart';
|
||||
export 'src/mixins/support_tracking.dart';
|
||||
export 'src/navigation/navigation_tracking.dart';
|
||||
export 'src/navigation/sf_router_listener.dart';
|
||||
export 'src/providers/crashlytics_service_provider.dart';
|
||||
export 'src/providers/sf_tracking_provider.dart';
|
||||
export 'src/sf_tracking_repository.dart';
|
||||
export 'src/tracking.dart';
|
||||
|
||||
106
packages/sf_tracking/lib/src/clients/crashlytics_service.dart
Normal file
106
packages/sf_tracking/lib/src/clients/crashlytics_service.dart
Normal file
@@ -0,0 +1,106 @@
|
||||
import 'package:firebase_crashlytics/firebase_crashlytics.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
|
||||
abstract class CrashlyticsService {
|
||||
Future<void> recordError(
|
||||
Object exception,
|
||||
StackTrace? stackTrace, {
|
||||
bool fatal = false,
|
||||
Iterable<Object> information = const [],
|
||||
String? reason,
|
||||
});
|
||||
|
||||
Future<void> recordFlutterError(
|
||||
FlutterErrorDetails details, {
|
||||
bool fatal = false,
|
||||
});
|
||||
|
||||
Future<void> log(String message);
|
||||
|
||||
Future<void> setUserIdentifier(String identifier);
|
||||
|
||||
Future<void> setCustomKey(String key, Object value);
|
||||
}
|
||||
|
||||
class FirebaseCrashlyticsService implements CrashlyticsService {
|
||||
FirebaseCrashlyticsService({required this.enabled});
|
||||
|
||||
final bool enabled;
|
||||
|
||||
@override
|
||||
Future<void> recordError(
|
||||
Object exception,
|
||||
StackTrace? stackTrace, {
|
||||
bool fatal = false,
|
||||
Iterable<Object> information = const [],
|
||||
String? reason,
|
||||
}) async {
|
||||
if (!enabled) return;
|
||||
await FirebaseCrashlytics.instance.recordError(
|
||||
exception,
|
||||
stackTrace,
|
||||
fatal: fatal,
|
||||
information: information,
|
||||
reason: reason,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> recordFlutterError(
|
||||
FlutterErrorDetails details, {
|
||||
bool fatal = false,
|
||||
}) async {
|
||||
if (!enabled) return;
|
||||
if (fatal) {
|
||||
await FirebaseCrashlytics.instance.recordFlutterFatalError(details);
|
||||
} else {
|
||||
await FirebaseCrashlytics.instance.recordFlutterError(details);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> log(String message) async {
|
||||
if (!enabled) return;
|
||||
await FirebaseCrashlytics.instance.log(message);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setUserIdentifier(String identifier) async {
|
||||
if (!enabled) return;
|
||||
await FirebaseCrashlytics.instance.setUserIdentifier(identifier);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> setCustomKey(String key, Object value) async {
|
||||
if (!enabled) return;
|
||||
await FirebaseCrashlytics.instance.setCustomKey(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
class NoopCrashlyticsService implements CrashlyticsService {
|
||||
const NoopCrashlyticsService();
|
||||
|
||||
@override
|
||||
Future<void> recordError(
|
||||
Object exception,
|
||||
StackTrace? stackTrace, {
|
||||
bool fatal = false,
|
||||
Iterable<Object> information = const [],
|
||||
String? reason,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> recordFlutterError(
|
||||
FlutterErrorDetails details, {
|
||||
bool fatal = false,
|
||||
}) async {}
|
||||
|
||||
@override
|
||||
Future<void> log(String message) async {}
|
||||
|
||||
@override
|
||||
Future<void> setUserIdentifier(String identifier) async {}
|
||||
|
||||
@override
|
||||
Future<void> setCustomKey(String key, Object value) async {}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import '../clients/crashlytics_service.dart';
|
||||
|
||||
// TODO(gdpr): wire `enabled` to the real user consent once the fix in backlog
|
||||
// lands (see sf-legacy-migration-checklist backlog). Hardcoded `true` today
|
||||
// matches the pre-refactor behaviour.
|
||||
final crashlyticsServiceProvider = Provider<CrashlyticsService>((ref) {
|
||||
return FirebaseCrashlyticsService(enabled: true);
|
||||
});
|
||||
@@ -12,11 +12,13 @@ dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_analytics: ^12.2.0
|
||||
firebase_crashlytics: ^5.1.0
|
||||
flutter_riverpod: ^3.0.3
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
flutter_lints: ^5.0.0
|
||||
mocktail: ^1.0.4
|
||||
|
||||
flutter:
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:sf_tracking/sf_tracking.dart';
|
||||
|
||||
void main() {
|
||||
group('NoopCrashlyticsService', () {
|
||||
test('recordError completes without side effects', () async {
|
||||
const service = NoopCrashlyticsService();
|
||||
await service.recordError(Exception('x'), StackTrace.current);
|
||||
});
|
||||
|
||||
test('recordFlutterError completes without side effects', () async {
|
||||
const service = NoopCrashlyticsService();
|
||||
await service.recordFlutterError(
|
||||
FlutterErrorDetails(exception: Exception('x')),
|
||||
);
|
||||
});
|
||||
|
||||
test('log / setUserIdentifier / setCustomKey are no-ops', () async {
|
||||
const service = NoopCrashlyticsService();
|
||||
await service.log('hello');
|
||||
await service.setUserIdentifier('u1');
|
||||
await service.setCustomKey('k', 'v');
|
||||
});
|
||||
});
|
||||
|
||||
group('FirebaseCrashlyticsService with enabled=false', () {
|
||||
test('recordError is a no-op when disabled', () async {
|
||||
final service = FirebaseCrashlyticsService(enabled: false);
|
||||
await service.recordError(Exception('x'), StackTrace.current);
|
||||
});
|
||||
|
||||
test('log is a no-op when disabled', () async {
|
||||
final service = FirebaseCrashlyticsService(enabled: false);
|
||||
await service.log('hello');
|
||||
});
|
||||
|
||||
test('setUserIdentifier is a no-op when disabled', () async {
|
||||
final service = FirebaseCrashlyticsService(enabled: false);
|
||||
await service.setUserIdentifier('u1');
|
||||
});
|
||||
|
||||
test('setCustomKey is a no-op when disabled', () async {
|
||||
final service = FirebaseCrashlyticsService(enabled: false);
|
||||
await service.setCustomKey('k', 'v');
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user