testdrive 0.1.0
testdrive: ^0.1.0 copied to clipboard
Flutter SDK for TestDeck. Captures HTTP, logs, navigation, and exceptions from debug builds.
TestDrive
Flutter SDK for TestDeck - stream HTTP, logs, navigation, and errors from your debug app.
The SDK runs inside your debug build and reports to the TestDeck desktop. No HTTPS proxy, no CA on the device.
Features #
- HTTP capture via
HttpOverrides- everydart:ioHttpClientis wrapped, including packages that use the default Dart IO client. Pre-TLS, so cert pinning is irrelevant. - Log capture -
debugPrintis wrapped automatically;TestDrive.log()for explicit structured entries;TestDrive.developerLog()as a drop-in fordart:developer.log. - Navigation capture - a drop-in
NavigatorObserverrecords push / pop / replace / remove with route names and JSON-safe arguments. - Error capture -
FlutterError.onErrorandPlatformDispatcher.instance.onErrorare chained; your existing handlers still fire. - Zero third-party dependencies - only Dart SDK libraries plus Flutter.
- Never blocks your app - non-blocking emit, a 128-event reconnect buffer, and full-jitter exponential backoff. If the desktop isn't running, events buffer and flush on reconnect.
- Rate limited - a default 200 events/s cap keeps a
debugPrintflood from swamping the transport. - Size-capped bodies - request/response bodies are tee'd into a 1 MB (configurable) buffer that never disturbs your app's own stream.
- NDJSON-over-TCP wire protocol with a version field for forward compatibility.
Platform Support #
| Platform | Transport | Status |
|---|---|---|
| Android | adb reverse (TCP loopback) |
Supported |
| Web | - | Not supported (dart:io HttpOverrides unavailable) |
Getting Started #
Installation #
dependencies:
testdrive: ^0.1.0
Or run:
flutter pub add testdrive
Basic Usage #
import 'package:flutter/foundation.dart';
import 'package:testdrive/testdrive.dart';
void main() {
// Guard with kDebugMode so the SDK never ships to production.
if (kDebugMode) TestDrive.init(appName: 'My App');
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
navigatorObservers: [TestDrive.navigator()],
home: const HomeScreen(),
);
}
}
appNameis just the label shown in the TestDeck desktop. It defaults to'unknown'; the SDK does not auto-detect the package name (that would add a platform-channel dependency).
Usage #
Signals #
| Signal | How it is captured |
|---|---|
| HTTP | HttpOverrides.global wraps every HttpClient - method, URL, headers, bodies, status, duration. |
| Logs | debugPrint wrapped + TestDrive.log() + TestDrive.developerLog(). |
| Navigation | TestDrive.navigator() added to MaterialApp.navigatorObservers. |
| Errors | FlutterError.onError + PlatformDispatcher.instance.onError, chained. |
Structured logs #
TestDrive.log(
'submit pressed',
level: LogLevel.info,
tag: 'checkout',
payload: {'items': 3, 'total': 42.99},
);
dart:developer.log #
Swap developer.log(...) for the drop-in so entries land in the TestDrive Logs tab. It
forwards to the real developer.log, so DevTools and the console are unaffected.
// before: developer.log('fetched user', name: 'auth', level: 900);
TestDrive.developerLog('fetched user', name: 'auth', level: 900);
Transparent interception of arbitrary
developer.log()calls would require the VM Service (vm_service), which this zero-dependency package avoids. Use the drop-in.
Timeline markers, timing & lifecycle #
Drop named markers onto the timeline to correlate app milestones with HTTP / logs /
navigation, and measure how long a block takes. Both route through the log stream with
a dedicated tag (event / timing), so they're filterable in the Logs tab.
TestDrive.event('checkout_started', payload: {'items': 3, 'total': 42.99});
final user = await TestDrive.timed('load_user', () => api.fetchUser());
App lifecycle transitions (resumed / inactive / paused / hidden / detached) are
captured automatically under the lifecycle tag - handy for seeing exactly when the
app went to the background. Disable with TestDrive.init(captureLifecycle: false).
Transport #
Events stream over TCP to 127.0.0.1:9876 (configurable). The TestDeck desktop runs
adb reverse tcp:9876 tcp:9876 automatically so the device's 127.0.0.1:9876 reaches
the host's listener. If the desktop isn't running, the SDK buffers up to 128 events
and reconnects with full-jitter exponential backoff - your app is never blocked.
API Reference #
| Method | Description |
|---|---|
TestDrive.init() |
Install HTTP / log / error hooks. Call once in main(), guarded by kDebugMode. Idempotent. |
TestDrive.navigator() |
Drop-in NavigatorObserver for MaterialApp.navigatorObservers. |
TestDrive.log() |
Emit an explicit structured log entry. |
TestDrive.event() |
Drop a named timeline marker (tag event) for milestone correlation. |
TestDrive.timed() |
Measure a sync/async block and emit its duration (tag timing). |
TestDrive.developerLog() |
Drop-in for dart:developer.log - forwards to the real call and captures it. |
TestDrive.detach() |
Restore previous globals and close the transport (mostly for tests). |
TestDrive.status |
Runtime diagnostics: connected, buffered, dropped, flushed. |
TestDrive.isInitialized |
Whether init() has run. |
Caveats #
- Debug mode only.
TestDrive.init()no-ops in release builds. Still guard your call withif (kDebugMode)so the intent is explicit and tree shaking can remove debug-only wiring. - No redaction. Captured payloads contain the literal request / response. Review before sharing exported sessions externally.
Limitations #
- Native plugin HTTP that runs outside Dart is invisible to
HttpOverrides. - HTTP from a background isolate is not captured -
HttpOverrides.globalanddebugPrintare per-isolate. - WebSocket handshakes are captured; subsequent frames are not parsed in v1.
- HTTP/2 multiplexing works, but each captured event is one request.
Contributing #
Contributions are welcome. Please open an issue before submitting a pull request.
License #
MIT License - Copyright (c) 2026 Ricky Irfandi. See LICENSE for details.