feat(sf_tracking): consent-aware crashlytics wrapper

This commit is contained in:
2026-04-21 19:33:54 +02:00
parent f89bca99b3
commit b21b234b9a
6 changed files with 174 additions and 3 deletions

View File

@@ -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';

View 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 {}
}

View File

@@ -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);
});

View File

@@ -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:

View File

@@ -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');
});
});
}