mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(mobile): default crash reporting on
Co-authored-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co> Signed-off-by: npub1ft62tztwwm2x9xamk25smmuaj4sfckdkldksruf2x2jwqalffkrq0g7arr <4af4a5896e76d4629bbbb2a90def9d95609c59b6fb6d01f12a32a4e077e94d86@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
parent
63a9348629
commit
a072957adb
@@ -108,19 +108,16 @@ class SettingsPage extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
|
||||
// Diagnostics
|
||||
// Crash reporting
|
||||
AppListSection(
|
||||
label: 'Diagnostics',
|
||||
children: [
|
||||
AppListRow(
|
||||
icon: LucideIcons.activity,
|
||||
title: 'Share Crash Reports',
|
||||
title: 'Share crash reports',
|
||||
subtitle: diagnostics.isConfigured
|
||||
? 'Send crash details to help improve Buzz. Reports '
|
||||
'do not include screenshots, view hierarchy, '
|
||||
'breadcrumbs, or performance traces.'
|
||||
? 'Sent anonymously to help fix problems.'
|
||||
: 'Crash reporting is unavailable in this build.',
|
||||
subtitleMaxLines: 4,
|
||||
subtitleMaxLines: 2,
|
||||
trailing: Switch.adaptive(
|
||||
value: diagnostics.consentGranted,
|
||||
onChanged:
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'sentry_config.dart';
|
||||
|
||||
const diagnosticsConsentPreferenceKey = 'buzz_crash_reporting_consent';
|
||||
const _diagnosticsEnabledByDefault = true;
|
||||
|
||||
typedef DiagnosticsLog = void Function(String message);
|
||||
|
||||
@@ -38,7 +39,8 @@ class DiagnosticsController extends ChangeNotifier {
|
||||
_crashReporter = crashReporter,
|
||||
_log = log ?? debugPrint,
|
||||
_consentGranted =
|
||||
preferences.getBool(diagnosticsConsentPreferenceKey) ?? false;
|
||||
preferences.getBool(diagnosticsConsentPreferenceKey) ??
|
||||
_diagnosticsEnabledByDefault;
|
||||
|
||||
final SharedPreferences _preferences;
|
||||
final SentryConfig _config;
|
||||
|
||||
@@ -12,11 +12,11 @@ void main() {
|
||||
late _RecordingCrashReporter reporter;
|
||||
|
||||
Future<DiagnosticsController> createController({
|
||||
bool consent = false,
|
||||
bool? consent,
|
||||
String dsn = 'https://public@example.invalid/1',
|
||||
}) async {
|
||||
SharedPreferences.setMockInitialValues({
|
||||
diagnosticsConsentPreferenceKey: consent,
|
||||
diagnosticsConsentPreferenceKey: ?consent,
|
||||
});
|
||||
preferences = await SharedPreferences.getInstance();
|
||||
reporter = _RecordingCrashReporter();
|
||||
@@ -50,23 +50,30 @@ void main() {
|
||||
await tester.pump();
|
||||
}
|
||||
|
||||
testWidgets('diagnostics switch enables crash reporting', (tester) async {
|
||||
testWidgets('diagnostics switch defaults on with approved copy', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await createController();
|
||||
await controller.applyStartupConsent();
|
||||
await pumpSettings(tester, controller);
|
||||
|
||||
expect(find.text('DIAGNOSTICS'), findsOneWidget);
|
||||
expect(find.text('Share Crash Reports'), findsOneWidget);
|
||||
expect(
|
||||
find.text(
|
||||
'Send crash details to help improve Buzz. Reports do not include '
|
||||
'screenshots, view hierarchy, breadcrumbs, or performance traces.',
|
||||
),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('personal information'), findsNothing);
|
||||
expect(find.textContaining('message text'), findsNothing);
|
||||
expect(find.text('DIAGNOSTICS'), findsNothing);
|
||||
expect(find.text('Share crash reports'), findsOneWidget);
|
||||
expect(find.text('Sent anonymously to help fix problems.'), findsOneWidget);
|
||||
expect(find.text('Share Crash Reports'), findsNothing);
|
||||
final toggle = tester.widget<Switch>(find.byType(Switch));
|
||||
expect(toggle.value, isFalse);
|
||||
expect(toggle.value, isTrue);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
||||
expect(reporter.initializeCalls, 1);
|
||||
});
|
||||
|
||||
testWidgets('explicit opt-in persists and initializes immediately', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await createController(consent: false);
|
||||
await pumpSettings(tester, controller);
|
||||
|
||||
expect(tester.widget<Switch>(find.byType(Switch)).value, isFalse);
|
||||
|
||||
await tester.tap(find.byType(Switch));
|
||||
await tester.pumpAndSettle();
|
||||
@@ -91,13 +98,14 @@ void main() {
|
||||
|
||||
expect(tester.widget<Switch>(find.byType(Switch)).value, isFalse);
|
||||
expect(controller.consentGranted, isFalse);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
||||
expect(reporter.closeCalls, 1);
|
||||
});
|
||||
|
||||
testWidgets('reports and rolls back an initialization failure', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await createController();
|
||||
final controller = await createController(consent: false);
|
||||
reporter.initializeError = StateError('init failed');
|
||||
await pumpSettings(tester, controller);
|
||||
|
||||
@@ -132,10 +140,34 @@ void main() {
|
||||
expect(reporter.initializeCalls, 0);
|
||||
});
|
||||
|
||||
testWidgets('diagnostics switch is disabled without a release DSN', (
|
||||
testWidgets('default-on preference is visible without a release DSN', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await createController(dsn: '');
|
||||
await controller.applyStartupConsent();
|
||||
await pumpSettings(tester, controller);
|
||||
|
||||
expect(
|
||||
find.text('Crash reporting is unavailable in this build.'),
|
||||
findsOneWidget,
|
||||
);
|
||||
final toggle = tester.widget<Switch>(find.byType(Switch));
|
||||
expect(toggle.value, isTrue);
|
||||
expect(toggle.onChanged, isNotNull);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
||||
expect(reporter.initializeCalls, 0);
|
||||
|
||||
await tester.tap(find.byType(Switch));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(controller.consentGranted, isFalse);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
||||
});
|
||||
|
||||
testWidgets('explicit opt-out is disabled without a release DSN', (
|
||||
tester,
|
||||
) async {
|
||||
final controller = await createController(consent: false, dsn: '');
|
||||
await pumpSettings(tester, controller);
|
||||
|
||||
expect(
|
||||
|
||||
@@ -30,30 +30,54 @@ void main() {
|
||||
);
|
||||
}
|
||||
|
||||
test(
|
||||
'defaults consent to false and does not initialize at startup',
|
||||
() async {
|
||||
final controller = await createController();
|
||||
test('missing preference defaults reporting on for first installs', () async {
|
||||
final controller = await createController();
|
||||
|
||||
await controller.applyStartupConsent();
|
||||
await controller.applyStartupConsent();
|
||||
|
||||
expect(controller.consentGranted, isFalse);
|
||||
expect(reporter.initializeCalls, 0);
|
||||
expect(reporter.closeCalls, 0);
|
||||
expect(
|
||||
logs,
|
||||
contains(
|
||||
'Diagnostics disabled: user consent is off; Sentry not initialized.',
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test('empty DSN prevents initialization even with stored consent', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: true},
|
||||
dsn: ' ',
|
||||
expect(controller.consentGranted, isTrue);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
||||
expect(reporter.initializeCalls, 1);
|
||||
expect(reporter.closeCalls, 0);
|
||||
expect(
|
||||
logs,
|
||||
contains('Diagnostics enabled: Sentry initialized after user consent.'),
|
||||
);
|
||||
});
|
||||
|
||||
test('missing preference defaults reporting on after an upgrade', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {'existing_preference_from_older_version': true},
|
||||
);
|
||||
|
||||
await controller.applyStartupConsent();
|
||||
|
||||
expect(controller.consentGranted, isTrue);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
||||
expect(reporter.initializeCalls, 1);
|
||||
});
|
||||
|
||||
test('explicitly stored revocation remains off at startup', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
);
|
||||
|
||||
await controller.applyStartupConsent();
|
||||
|
||||
expect(controller.consentGranted, isFalse);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
||||
expect(reporter.initializeCalls, 0);
|
||||
expect(reporter.closeCalls, 0);
|
||||
expect(
|
||||
logs,
|
||||
contains(
|
||||
'Diagnostics disabled: user consent is off; Sentry not initialized.',
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test('empty DSN prevents initialization with default-on reporting', () async {
|
||||
final controller = await createController(dsn: ' ');
|
||||
|
||||
await controller.applyStartupConsent();
|
||||
|
||||
@@ -68,13 +92,16 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('cannot enable consent in an unconfigured build', () async {
|
||||
final controller = await createController(dsn: ' ');
|
||||
test('cannot opt in to an unconfigured build after revocation', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
dsn: ' ',
|
||||
);
|
||||
|
||||
await expectLater(controller.setConsent(true), throwsA(isA<StateError>()));
|
||||
|
||||
expect(controller.consentGranted, isFalse);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isNull);
|
||||
expect(preferences.getBool(diagnosticsConsentPreferenceKey), isFalse);
|
||||
expect(reporter.initializeCalls, 0);
|
||||
expect(
|
||||
logs,
|
||||
@@ -101,8 +128,10 @@ void main() {
|
||||
);
|
||||
});
|
||||
|
||||
test('enabling persists consent and initializes immediately', () async {
|
||||
final controller = await createController();
|
||||
test('explicit opt-in persists and initializes immediately', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
);
|
||||
|
||||
await controller.setConsent(true);
|
||||
|
||||
@@ -146,7 +175,9 @@ void main() {
|
||||
});
|
||||
|
||||
test('serializes concurrent consent changes', () async {
|
||||
final controller = await createController();
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
);
|
||||
reporter.initializeGate = Completer<void>();
|
||||
|
||||
final enable = controller.setConsent(true);
|
||||
@@ -163,8 +194,10 @@ void main() {
|
||||
expect(reporter.closeCalls, 1);
|
||||
});
|
||||
|
||||
test('initialization failure rolls back consent', () async {
|
||||
final controller = await createController();
|
||||
test('initialization failure preserves explicit revocation', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
);
|
||||
reporter.initializeError = StateError('init failed');
|
||||
|
||||
await expectLater(controller.setConsent(true), throwsA(isA<StateError>()));
|
||||
@@ -189,7 +222,9 @@ void main() {
|
||||
});
|
||||
|
||||
test('continues accepting changes after a failed operation', () async {
|
||||
final controller = await createController();
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
);
|
||||
reporter.initializeError = StateError('init failed');
|
||||
|
||||
await expectLater(controller.setConsent(true), throwsA(isA<StateError>()));
|
||||
@@ -201,8 +236,10 @@ void main() {
|
||||
expect(reporter.initializeCalls, 2);
|
||||
});
|
||||
|
||||
test('repeating the current value is idempotent', () async {
|
||||
final controller = await createController();
|
||||
test('repeating explicit values is idempotent', () async {
|
||||
final controller = await createController(
|
||||
storedValues: {diagnosticsConsentPreferenceKey: false},
|
||||
);
|
||||
|
||||
await controller.setConsent(false);
|
||||
await controller.setConsent(true);
|
||||
|
||||
Reference in New Issue
Block a user