Three new pieces, all composable through the existing Log API
(`Log.configure(sink: ConsoleSink() + HttpSink(...))`) — no new
facade, no install side-effects.
HttpSink (lib/src/log_http.dart)
- Extends LogSink. Batches records and POSTs them as a JSON array
to a configurable endpoint with bearer auth.
- Defaults: batchSize=25, flushInterval=2s, queueCapacity=2000,
initialBackoff=1s capped at maxBackoff=30s.
- Drops oldest on queue overflow (single console warning).
- Retries 5xx and network errors with exponential backoff; drops
on 4xx with a single console warning.
- Pluggable `HttpSender` typedef for tests; default uses
dart:io.HttpClient.
CrashPlugin (ios/Classes/CrashPlugin.swift,
android/src/main/kotlin/.../CrashPlugin.kt)
- Installs uncaught-exception handlers
(NSSetUncaughtExceptionHandler / Thread.UncaughtExceptionHandler),
chains to the prior handler so the platform's default kill path
still runs.
- Writes one JSON file per crash to <cacheDir>/ux_crashes/<uuid>.json.
iOS captures NSException.name/reason/userInfo + call-stack symbols
and return addresses. Android captures thread name, exception
class, message, full stack (including cause chain).
- Caps the directory at 50 files; drops oldest by mtime on overflow.
- Exposes method channel `ux/crash` with drainPending / ackCrash /
triggerTestCrash. Registered in UxPlugin on both platforms.
UxCrash.drainAndReport (lib/src/crash.dart)
- Pulls persisted crash records on boot, re-emits each via Log.f
(tag `ux.crash`) so they flow out through whatever sink chain
the app installed, then acks each id.
- Tolerates MissingPluginException silently; PlatformException is
logged as a single warn without throwing.
Tests:
- log_http_test.dart: payload shape, batching, retry doubling on 5xx,
drop on 4xx, queue overflow ordering, non-encodable field
stringification, real loopback HTTP round-trip with the default
sender.
- log_http_e2e_test.dart: opt-in real-server round-trip gated by
--dart-define=E2E_LOG_ENDPOINT/E2E_LOG_TOKEN.
- crash_test.dart: drain + re-emit + ack across iOS and Android
shapes, MissingPluginException tolerance, PlatformException
warn-not-throw.
34 lines
1021 B
Dart
34 lines
1021 B
Dart
@Tags(['e2e'])
|
|
library;
|
|
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:ux/ux.dart';
|
|
|
|
/// End-to-end smoke against a running `tools/log_server` at $E2E_LOG_ENDPOINT.
|
|
/// Skipped unless both `E2E_LOG_ENDPOINT` and `E2E_LOG_TOKEN` are provided.
|
|
void main() {
|
|
const endpoint = String.fromEnvironment('E2E_LOG_ENDPOINT');
|
|
const token = String.fromEnvironment('E2E_LOG_TOKEN');
|
|
final skip = endpoint.isEmpty || token.isEmpty
|
|
? 'Set --dart-define=E2E_LOG_ENDPOINT and E2E_LOG_TOKEN to run'
|
|
: false;
|
|
|
|
test('round-trip through HttpSink → log_server', () async {
|
|
final sink = HttpSink(
|
|
endpoint: Uri.parse('$endpoint/log'),
|
|
token: token,
|
|
device: {'platform': 'test'},
|
|
app: {'version': 'e2e'},
|
|
flushInterval: const Duration(milliseconds: 1),
|
|
);
|
|
|
|
sink.emit(LogRecord(
|
|
time: DateTime.utc(2026, 5, 10, 12, 0, 0),
|
|
level: LogLevel.error,
|
|
message: 'e2e-roundtrip',
|
|
tag: 'smoke',
|
|
));
|
|
await sink.flush();
|
|
}, skip: skip);
|
|
}
|