Watchdog

A Flutter developer toolkit that streams HTTP requests, responses, BLoC lifecycle events, navigation, and app logs to a browser-based DevTools page in real-time. No IDE plugins, no native tooling, no account.

flutter run                watchdog open
   │                           │
   ▼                           ▼
┌───────────────┐         ┌───────────────┐
│ your app      │◀────────│ browser tab   │
│  Watchdog     │   :8888 │  localhost    │
└───────────────┘         └───────────────┘

2-minute quickstart

Five steps. Copy-paste each block as you go.

1. Add the dependency

pubspec.yaml:

dependencies:
  watchdog: ^0.2.0
flutter pub get

2. Start before runApp

main.dart — pick the pattern that fits your project:

Option A — one-liner (recommended)

import 'package:watchdog/watchdog.dart';

// Replaces ensureInitialized + Watchdog.start + runApp in one call.
void main() => runWatchLocalApp(
  const MyApp(),
  stateManagement: StateManagement.bloc, // or .riverpod / .both / .none
);

runWatchLocalApp handles WidgetsFlutterBinding.ensureInitialized(), Watchdog.start(), BLoC/Riverpod observer wiring, and runApp for you.

Option B — manual (more control)

import 'package:flutter/widgets.dart';
import 'package:watchdog/watchdog.dart';

Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Watchdog.start(); // one call — initializes on first use

  runApp(const MyApp());
}

Either way, Watchdog only runs in debug builds — release builds are a no-op, so it's safe to leave in production code.

3. Wire the observers where you already use your HTTP client / Navigator

Only add the lines you need. Skip the ones that don't apply.

// HTTP inspection — Chopper:
ChopperClient(interceptors: [Watchdog.chopperInterceptor]);

// HTTP inspection — Dio (same Network tab, identical view):
final dio = Dio()..interceptors.add(Watchdog.dioInterceptor);

// State management — Riverpod (Instances tab):
ProviderScope(observers: [Watchdog.providerObserver], child: const MyApp());

// Navigation events:
MaterialApp(navigatorObservers: [Watchdog.routeObserver]);

// DI registrations (GetIt) — after configureDependencies():
Watchdog.trackGetIt(getIt);

State management is optional and pluggable — use BLoC/Cubit, Riverpod, or both. BLoC/Cubit lifecycle is wired automatically by Watchdog.start(); for Riverpod, add Watchdog.providerObserver to your ProviderScope. Both feed the same Instances tab.

4. Install the CLI (once per machine)

dart pub global activate watchdog

Then add pub's bin directory to your PATH. Pick the block for your shell (see PATH setup details if unsure):

macOS / Linux (zsh or bash)
echo 'export PATH="$PATH:$HOME/.pub-cache/bin"' >> ~/.zshrc
source ~/.zshrc

Use ~/.bashrc instead of ~/.zshrc if you're on bash.

Windows — Git Bash
echo 'watchdog() { "$(cygpath "$LOCALAPPDATA/Pub/Cache/bin/watchdog.bat")" "$@"; }' >> ~/.bashrc
source ~/.bashrc
Windows — PowerShell
[Environment]::SetEnvironmentVariable("Path", $env:Path + ";$env:LOCALAPPDATA\Pub\Cache\bin", "User")

Close and reopen the terminal afterwards.

Verify:

watchdog version

5. Open DevTools

With the app running on a device or emulator:

watchdog open

Your browser opens http://localhost:8888. You're done.


What you get

  • Network inspector — every Chopper or Dio HTTP request/response with headers, body, status, timing, and cURL export.
  • BLoC lifecycle — creation, state changes, and closure of every Bloc and Cubit, with creation stack traces.
  • Riverpod lifecycle — provider init, updates, and disposal in the same Instances tab (optional; wire Watchdog.providerObserver).
  • Navigation — push, pop, replace, remove events with a live route stack.
  • Structured logging — debug / info / warning / error / critical levels.
  • WebSocket tracker — send, receive, and connection state events.
  • GetIt scanner — broadcasts every DI registration.
  • Cloud streaming — optionally mirror events to a remote server.
  • Replay buffer — late-connecting browsers receive full event history.

Daily usage

watchdog open               # the one you'll use 99% of the time
watchdog open --port 9000   # custom port
watchdog open --no-adb      # iOS-only / real-WiFi (skip adb)
watchdog forward            # set up port forwarding, no browser
watchdog devices            # list adb devices
watchdog version            # print CLI version

Convenience logging

Watchdog.debug('Cache miss for key=$key');
Watchdog.info('Driver connected');
Watchdog.warning('GPS accuracy degraded: ${accuracy}m');
Watchdog.error('Sync failed', error: e, stackTrace: st);

WebSocket tracking

Watchdog.socketTracker.trackSend(channel: 'wss://api.example.com/ws', data: payload);
Watchdog.socketTracker.trackReceive(channel: 'wss://api.example.com/ws', data: decoded);

Configuration

Pass a WatchdogConfig to initialize() to override defaults:

await Watchdog.initialize(
  config: const WatchdogConfig(
    apiBaseUrl: 'https://api.example.com',
    enableLogging: true,
  ),
);
Parameter Default Description
apiBaseUrl null Shown in the DevTools header for environment ID
port 8888 Local server port
host 0.0.0.0 Bind address
enableLogging true Enable log events in the Logs tab
maskSensitiveHeaders true Mask Authorization headers in the UI
replayBufferSize 10000 Max events retained for late clients
enabled null null = kDebugMode; true forces enable
global false When true, skips local server — streams to cloud only
cloud null WatchdogCloudConfig for remote streaming
device null WatchdogDevice — identifies this instance in the cloud dashboard

Advanced

Custom logger — integrate your own logging backend
class MyLogger implements WatchdogLogger {
  @override
  void log(WatchdogLogLevel level, String message,
      {String? title, Object? error, StackTrace? stackTrace}) {
    // your implementation
  }
}

await Watchdog.initialize(
  dependencies: WatchdogDependencies(logger: MyLogger()),
);
Cloud streaming (local + cloud) — mirror events to a remote server alongside local DevTools
await Watchdog.initialize(
  config: const WatchdogConfig(
    cloud: WatchdogCloudConfig(
      serverUrl: 'wss://watchdog-cloud.example.com',
      apiKey: 'your-api-key',
      appName: 'my-app',
    ),
  ),
);
Global (cloud-only) mode — stream remotely, skip localhost server

Use runWatchGlobalApp to disable the local server and stream only to the cloud. Useful for observing devices that aren't physically connected to your machine.

import 'package:watchdog/watchdog.dart';

void main() => runWatchGlobalApp(
  const MyApp(),
  cloud: const WatchdogCloudConfig(
    serverUrl: 'wss://watchdog-cloud.example.com',
    apiKey: 'your-api-key',
    appName: 'my-app',
  ),
  device: WatchdogDevice(deviceName: 'Pixel 7', appVersion: '1.0.0'),
  stateManagement: StateManagement.both,
);

Or manually with WatchdogConfig(global: true, cloud: ...).

WatchdogDevice — identify instances in the cloud dashboard
WatchdogDevice(
  deviceName: 'Samsung Galaxy S24',
  appVersion: '2.1.0',
  // optional: platform, osVersion, etc. auto-detected when omitted
)

Pass this to runWatchGlobalApp(device: ...) or WatchdogConfig(device: ...).

Bridge an existing project logger
AppLogger.attachBridge(Watchdog.bridge);

Troubleshooting

watchdog: command not found The pub cache bin directory isn't on your PATH. Redo step 4.

Browser shows "This site can't be reached" / ERR_CONNECTION_REFUSED Three things to check, in order:

  1. The Flutter app is running on a device/emulator (not just built).
  2. You're on a debug build (flutter run without --release).
  3. adb devices lists your device. If adb isn't on PATH, install Android platform-tools or use watchdog open --no-adb for iOS.

adb forward failed: cannot bind listener A previous forwarding is stuck. Run:

adb forward --remove-all && watchdog open

PATH setup details

dart pub global activate installs the watchdog executable into the Dart pub cache, but does not add that directory to your PATH. Until you do it yourself, typing watchdog won't find the binary.

The pub cache lives at:

  • macOS / Linux: ~/.pub-cache/bin
  • Windows: %LOCALAPPDATA%\Pub\Cache\bin

On Windows + Git Bash, simply adding that directory to PATH isn't enough because pub installs the executable as watchdog.bat and Git Bash doesn't auto-append .bat when resolving commands. That's why step 4's Git Bash block defines a shell function that invokes the .bat file directly — it works regardless of extension handling.


Public API summary

Top-level helpers

Function Description
runWatchLocalApp(app, ...) One-liner boot: local DevTools + optional BLoC/Riverpod wiring
runWatchGlobalApp(app, cloud:, ...) One-liner boot: cloud-only mode (no localhost server)

StateManagement enum values: none · bloc · riverpod · both

Watchdog class

Accessor Type Description
Watchdog.initialize() Future<void> One-time setup (optional — start() does it)
Watchdog.start() Future<void> Initializes if needed + opens server + cloud
Watchdog.stop() Future<void> Pauses capture
Watchdog.dispose() Future<void> Full teardown
Watchdog.logger WatchdogLogger Active logger instance
Watchdog.bridge WatchdogLoggerBridge Legacy logger adapter
Watchdog.blocObserver WatchdogBlocObserver BLoC/Cubit lifecycle observer
Watchdog.providerObserver WatchdogProviderObserver Riverpod lifecycle observer
Watchdog.routeObserver WatchdogRouteObserver Navigation observer
Watchdog.chopperInterceptor WatchdogChopperInterceptor Chopper HTTP interceptor
Watchdog.dioInterceptor WatchdogDioInterceptor Dio HTTP interceptor
Watchdog.socketTracker WatchdogSocketTracker WebSocket tracker
Watchdog.trackGetIt() void Scans GetIt container
Watchdog.isInitialized bool Init state
Watchdog.isRunning bool Server state

License

MIT — see LICENSE.

Libraries

watchdog
Watchdog — Flutter developer toolkit.