Sniffer is a Kotlin Multiplatform SDK and local monitor for inspecting and mocking mobile network traffic while you develop apps.
It supports OkHttp, Ktor Client, Socket.IO, and Ktor WebSocket.
It follows the developer workflow that made Flipper useful: connect a debug app, watch traffic live, and change behavior from a desktop UI. Sniffer exists because our Flipper-based Android setup ran into the Android 16 KB page-size migration, while the upstream Flipper repository is now a public archive/read-only project. Sniffer keeps the network debugging pieces in a small stack we can maintain.
The daemon runs on your dev machine: it serves the browser UI, stores mock
rules, and keeps adb reverse alive for Android devices.
Prerequisites
- Node.js 20+ (npm ships with it)
adb(Android platform-tools) on your PATH, for Android devices only; iOS needs nothing extra
npm install -g @dev-weiqi/sniffersniffer startThe UI opens in your browser at http://localhost:9091. Or skip the install and run it straight:
npx @dev-weiqi/sniffer startIf port 9091 is taken, Sniffer offers to free it for you. You can also pick another port:
PORT=9092 sniffer startBy default the daemon binds to 127.0.0.1 only, so it is not exposed to your
network. Android (via adb reverse) and the iOS simulator reach it over
localhost, so they are unaffected. A physical iOS/Android device connecting
over Wi-Fi to your machine's LAN address needs the daemon opened up:
SNIFFER_BIND=0.0.0.0 sniffer startSNIFFER_BIND (daemon: which interface to listen on) is the server-side twin of
the client-side host you pass in the app (Sniffer.start(host = ...), which
tells the device where to find the daemon). For Wi-Fi you set both: open the
daemon with SNIFFER_BIND=0.0.0.0, and point the app at your machine's LAN IP.
To update an existing install to the latest version:
npm install -g @dev-weiqi/sniffer@latestOr pin a specific version:
npm install -g @dev-weiqi/sniffer@0.4.1A macOS desktop build bundles the daemon and UI into one window, with no Node.js
or npm needed. Download the latest .dmg from the
Releases page
(Sniffer-server-mac-aarch64.dmg for Apple Silicon, Sniffer-server-mac-x64.dmg
for Intel), drag Sniffer.app into /Applications, and launch. It runs the same
daemon as the CLI; change port 9091 in the app's settings if needed.
The dmg is not notarized by Apple yet, so Gatekeeper blocks it after download.
Drag Sniffer.app into /Applications, then clear the quarantine flag once:
xattr -d com.apple.quarantine /Applications/Sniffer.appAfter that it opens normally.
Use core plus the transport modules your app actually uses. Add the matching
-noop artifacts to release builds so production builds keep the same API with
empty implementations.
Supported client integrations:
| Client | Sniffer artifact |
|---|---|
| OkHttp | io.github.dev-weiqi.sniffer:okhttp |
| Ktor Client | io.github.dev-weiqi.sniffer:ktor |
| Socket.IO | io.github.dev-weiqi.sniffer:socketio |
| Ktor WebSocket | io.github.dev-weiqi.sniffer:ktor-ws |
val snifferVersion = "0.4.1"
dependencies {
debugImplementation("io.github.dev-weiqi.sniffer:core:$snifferVersion")
releaseImplementation("io.github.dev-weiqi.sniffer:core-noop:$snifferVersion")
debugImplementation("io.github.dev-weiqi.sniffer:okhttp:$snifferVersion")
releaseImplementation("io.github.dev-weiqi.sniffer:okhttp-noop:$snifferVersion")
debugImplementation("io.github.dev-weiqi.sniffer:ktor:$snifferVersion")
releaseImplementation("io.github.dev-weiqi.sniffer:ktor-noop:$snifferVersion")
debugImplementation("io.github.dev-weiqi.sniffer:socketio:$snifferVersion")
releaseImplementation("io.github.dev-weiqi.sniffer:socketio-noop:$snifferVersion")
debugImplementation("io.github.dev-weiqi.sniffer:ktor-ws:$snifferVersion")
releaseImplementation("io.github.dev-weiqi.sniffer:ktor-ws-noop:$snifferVersion")
}For Kotlin Multiplatform shared code, add the KMP modules you need to
commonMain:
commonMain.dependencies {
implementation("io.github.dev-weiqi.sniffer:core:$snifferVersion")
implementation("io.github.dev-weiqi.sniffer:ktor:$snifferVersion")
implementation("io.github.dev-weiqi.sniffer:ktor-ws:$snifferVersion")
}Start the SDK once when the app boots. The SDK reconnects automatically and does not throw into your app when the daemon is not running.
import dev.weiqi.sniffer.core.Sniffer
class App : Application() {
override fun onCreate() {
super.onCreate()
Sniffer.start(appId = packageName)
}
}Android devices and emulators reach the daemon through adb reverse, so
localhost:9091 works by default. iOS simulators can also use localhost.
For a physical iOS device, pass your Mac's LAN address:
Sniffer.start(appId = "com.example.app", host = "192.168.1.20")Runtime overrides are available when you do not want to rebuild:
adb shell setprop debug.sniffer.port 9092
adb shell setprop debug.sniffer.host 192.168.1.20OkHttp:
import dev.weiqi.sniffer.okhttp.SnifferOkHttp
val okHttp = OkHttpClient.Builder()
.addInterceptor(SnifferOkHttp.interceptor())
.build()Ktor client:
import dev.weiqi.sniffer.ktor.SnifferKtor
val ktor = HttpClient(CIO) {
install(SnifferKtor)
}Socket.IO:
import dev.weiqi.sniffer.socketio.SnifferSocketIO
val raw = IO.socket("https://api.example.com")
val socket = SnifferSocketIO.wrap(raw, "https://api.example.com")
socket.connect()
socket.emit("cart:update", mapOf("sku" to "pro"))Apps that multiplex everything over one generic event can pass a label lambda
to on: it receives the args and returns a short tag the UI shows as
event(tag) in the list. Only the tag inside the parentheses is app-controlled,
and the real event name is always visible. The tag is display-only: listeners,
the wire event name, and mock matching all use the real name. Return null for
no tag:
socket.on("message", label = { args ->
args.optJSONObject(0)?.optString("type")?.takeIf { it.isNotEmpty() }
}) { args ->
// normal listener
}
// server sends: message {"type":"chat",...} → listed as message(chat)Ktor WebSocket: install the plugin once and plain webSocket calls are monitored:
import dev.weiqi.sniffer.ktorws.SnifferKtorWs
val ktor = HttpClient(CIO) {
install(SnifferKtorWs)
install(WebSockets)
}
val session = ktor.webSocketSession("wss://api.example.com/realtime")
session.send("ping")Open http://localhost:9091, launch your debug app, and select the connected
device.
- API: inspect live requests and responses, headers, JSON, images, animated WebP responses, copied cURL commands, and mocked entries.
- Socket: inspect Socket.IO and WebSocket connections, events, payloads, acks, and pushed server-to-client messages.
- Mocks: create per-device HTTP response rules, delay-only rules, Socket.IO ack rules, WebSocket reply rules, and server-to-client push events.
Rules are sent to the selected device and run inside the SDK. HTTP mocks
short-circuit matched requests before the network. Socket ack rules answer the
client locally. Mock bodies support placeholders such as ${id} and
${randomString(length)}.
| Artifact | Use it for |
|---|---|
io.github.dev-weiqi.sniffer:core |
SDK connection, device identity, mock rule sync |
io.github.dev-weiqi.sniffer:okhttp |
OkHttp request/response inspection and HTTP mocks |
io.github.dev-weiqi.sniffer:ktor |
Ktor client inspection for Android/iOS/JVM |
io.github.dev-weiqi.sniffer:socketio |
Socket.IO event inspection, ack mocks, push events |
io.github.dev-weiqi.sniffer:ktor-ws |
Ktor WebSocket frame inspection and reply mocks |
Each artifact has a -noop twin with the same API.
From this repository:
npm run setup
npm start
cd client
./gradlew :sample:installDebug
./gradlew :sample-cmp:installDebugUseful checks:
cd server/daemon && npm run typecheck
cd server/ui && npm run build
cd client && ./gradlew :core:jvmTest :okhttp:test :sample:compileDebugKotlinMore detail: docs/GUIDE.md and PROTOCOL.md.
