todo-app/lib/analytics/analytics_event.dart
Krzysztof kuhy Rudnicki ce2ea5b410 Simplify capture UI behind an advanced-mode toggle, add usage analytics
Hide priority/status/template/mode chrome and routine sync-status text
behind a Firebase-synced "Enable advanced" setting (default off); remove
the Capture title and Save button in favor of autosave + a plain reset
icon; sort notes by priority then recency; add interaction-only usage
analytics buffered locally and flushed to Firebase alongside note sync.

Code review (self + follow-up verification pass) found and fixed 3 real
bugs during this work: a widget-key placement bug that discarded an
in-progress draft when advancedMode was toggled mid-typing, a data-loss
bug in the analytics buffer's flush-and-clear logic under concurrent
cap-eviction, and two spots where an expired/wrong-account Firebase
session (FirebaseAuthError) escaped a narrower catch clause and would
have failed an otherwise-successful sync pass.

Also fixes an unrelated pre-existing test-isolation bug in
wrapper_server_test.dart, which was reading the real
~/.config/todo/firebase_auth.json instead of a test-scoped path.

320 tests, 100% line coverage on all touched files (one pre-existing,
out-of-scope 5-line gap in settings_screen.dart unrelated to this
change).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-12 21:30:40 +02:00

52 lines
1.7 KiB
Dart

/// A single, structured analytics event.
///
/// Interaction-only by construction: [params] holds enum values, counts,
/// durations and booleans — never note text or other free-form user input.
/// Callers are trusted to uphold that; see `analytics_service.dart` for the
/// full policy.
class AnalyticsEvent {
/// Creates an [AnalyticsEvent] with the given [name], [params] and
/// [timestamp].
const AnalyticsEvent({
required this.name,
required this.timestamp,
this.params = const {},
});
/// Short, stable event identifier, e.g. `'screen_view'`, `'sort_changed'`.
final String name;
/// When the event happened.
final DateTime timestamp;
/// Structured parameters. Values must be JSON-encodable primitives
/// (String, num, bool, or null) — never note text.
final Map<String, Object?> params;
/// Serializes to a JSON-encodable map for local buffering and the
/// Firebase flush.
Map<String, Object?> toJson() => {
'name': name,
'timestamp': timestamp.toIso8601String(),
'params': params,
};
/// Reconstructs an [AnalyticsEvent] from [toJson]'s output. Returns null
/// for a malformed entry (e.g. a buffer corrupted by a previous crash)
/// rather than throwing — analytics must never crash capture.
static AnalyticsEvent? tryFromJson(Object? json) {
if (json is! Map) return null;
final name = json['name'];
final timestamp = DateTime.tryParse('${json['timestamp']}');
final params = json['params'];
if (name is! String || timestamp == null) return null;
return AnalyticsEvent(
name: name,
timestamp: timestamp,
params: params is Map
? Map<String, Object?>.from(params)
: const <String, Object?>{},
);
}
}