Improve share intake diagnostics

This commit is contained in:
Rijad Zuzo
2026-05-19 11:26:23 +02:00
parent 0bfe67b1bf
commit 9d2da0c600
4 changed files with 120 additions and 7 deletions
@@ -520,12 +520,13 @@ class _ShareDiagnosticsDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: events children: events
.map( .map(
(ShareIntakeDebugEvent event) => Padding( (
padding: const EdgeInsets.only(bottom: 10), ShareIntakeDebugEvent event,
child: SelectableText( ) => _ShareDiagnosticsEventTile(
event: event,
label:
'${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}', '${_debugTimeLabel(event.at)} ${event.stage}\n${event.message}',
), ),
),
) )
.toList(growable: false), .toList(growable: false),
), ),
@@ -545,6 +546,51 @@ class _ShareDiagnosticsDialog extends StatelessWidget {
} }
} }
class _ShareDiagnosticsEventTile extends StatelessWidget {
const _ShareDiagnosticsEventTile({required this.event, required this.label});
final ShareIntakeDebugEvent event;
final String label;
@override
Widget build(BuildContext context) {
final String? details = event.details;
if (details == null || details.isEmpty) {
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: SelectableText(label),
);
}
return Padding(
padding: const EdgeInsets.only(bottom: 10),
child: ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: const EdgeInsets.only(top: 6, bottom: 8),
title: SelectableText(label),
subtitle: Text(
'Tap to show raw shared text (${details.length} chars).',
),
children: <Widget>[
DecoratedBox(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(6),
),
child: Padding(
padding: const EdgeInsets.all(10),
child: Align(
alignment: Alignment.centerLeft,
child: SelectableText(details),
),
),
),
],
),
);
}
}
String _debugTimeLabel(DateTime value) { String _debugTimeLabel(DateTime value) {
return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}'; return '${value.hour.toString().padLeft(2, '0')}:${value.minute.toString().padLeft(2, '0')}:${value.second.toString().padLeft(2, '0')}';
} }
@@ -4,12 +4,14 @@ import 'package:shared_preferences/shared_preferences.dart';
const String _shareIntakeDebugLogKey = 'share_intake_debug_log_v1'; const String _shareIntakeDebugLogKey = 'share_intake_debug_log_v1';
const int _maxShareIntakeDebugEvents = 40; const int _maxShareIntakeDebugEvents = 40;
const int _maxShareIntakeDebugDetailChars = 6000;
class ShareIntakeDebugEvent { class ShareIntakeDebugEvent {
const ShareIntakeDebugEvent({ const ShareIntakeDebugEvent({
required this.at, required this.at,
required this.stage, required this.stage,
required this.message, required this.message,
this.details,
}); });
factory ShareIntakeDebugEvent.fromJson(Map<String, dynamic> json) { factory ShareIntakeDebugEvent.fromJson(Map<String, dynamic> json) {
@@ -19,18 +21,21 @@ class ShareIntakeDebugEvent {
).toLocal(), ).toLocal(),
stage: json['stage'] as String? ?? 'unknown', stage: json['stage'] as String? ?? 'unknown',
message: json['message'] as String? ?? '', message: json['message'] as String? ?? '',
details: json['details'] as String?,
); );
} }
final DateTime at; final DateTime at;
final String stage; final String stage;
final String message; final String message;
final String? details;
Map<String, dynamic> toJson() { Map<String, dynamic> toJson() {
return <String, dynamic>{ return <String, dynamic>{
'at': at.toUtc().toIso8601String(), 'at': at.toUtc().toIso8601String(),
'stage': stage, 'stage': stage,
'message': message, 'message': message,
if (details != null) 'details': details,
}; };
} }
} }
@@ -55,11 +60,16 @@ class ShareIntakeDebugLog {
} }
} }
Future<void> record(String stage, String message) async { Future<void> record(String stage, String message, {String? details}) async {
final SharedPreferences prefs = await SharedPreferences.getInstance(); final SharedPreferences prefs = await SharedPreferences.getInstance();
final List<ShareIntakeDebugEvent> current = await read(); final List<ShareIntakeDebugEvent> current = await read();
final List<ShareIntakeDebugEvent> next = <ShareIntakeDebugEvent>[ final List<ShareIntakeDebugEvent> next = <ShareIntakeDebugEvent>[
ShareIntakeDebugEvent(at: DateTime.now(), stage: stage, message: message), ShareIntakeDebugEvent(
at: DateTime.now(),
stage: stage,
message: message,
details: _compactDetails(details),
),
...current, ...current,
].take(_maxShareIntakeDebugEvents).toList(growable: false); ].take(_maxShareIntakeDebugEvents).toList(growable: false);
await prefs.setString( await prefs.setString(
@@ -77,3 +87,14 @@ class ShareIntakeDebugLog {
await prefs.remove(_shareIntakeDebugLogKey); await prefs.remove(_shareIntakeDebugLogKey);
} }
} }
String? _compactDetails(String? value) {
final String? trimmed = value?.trim();
if (trimmed == null || trimmed.isEmpty) {
return null;
}
if (trimmed.length <= _maxShareIntakeDebugDetailChars) {
return trimmed;
}
return '${trimmed.substring(0, _maxShareIntakeDebugDetailChars)}\n[truncated]';
}
@@ -159,6 +159,12 @@ class _IncomingShareListenerState extends ConsumerState<IncomingShareListener> {
return; return;
} }
await _debugLog.record(
'raw_share_text',
'Captured raw share text (${trimmed.length} chars).',
details: trimmed,
);
final SharedPayload? payload = buildSharedPayloadFromRawText( final SharedPayload? payload = buildSharedPayloadFromRawText(
rawText: trimmed, rawText: trimmed,
platform: defaultTargetPlatform.name, platform: defaultTargetPlatform.name,
@@ -0,0 +1,40 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:relationship_saver/features/share_intake/application/share_intake_debug_log.dart';
import 'package:shared_preferences/shared_preferences.dart';
void main() {
setUp(() {
SharedPreferences.setMockInitialValues(<String, Object>{});
});
test('records optional share details for diagnostics', () async {
const ShareIntakeDebugLog log = ShareIntakeDebugLog();
await log.record(
'raw_share_text',
'Captured raw share text (18 chars).',
details: 'Alex: likes cycling',
);
final List<ShareIntakeDebugEvent> events = await log.read();
expect(events, hasLength(1));
expect(events.single.stage, 'raw_share_text');
expect(events.single.details, 'Alex: likes cycling');
});
test('trims very large details before storing', () async {
const ShareIntakeDebugLog log = ShareIntakeDebugLog();
await log.record(
'raw_share_text',
'large',
details: List<String>.filled(7000, 'x').join(),
);
final List<ShareIntakeDebugEvent> events = await log.read();
expect(events.single.details, endsWith('[truncated]'));
expect(events.single.details!.length, lessThan(7000));
});
}