Skip to content

Repository files navigation

Sharingan 👁️

Live HTTP · MQTT · Bluetooth logging for Kotlin Multiplatform — iOS & Android, one API, exportable as agent-ready Markdown.

Sharingan is an on-device debug logger for KMP apps. It captures protocol traffic while you use your app, then hands you clean, structured logs — method, URL, status, headers, bodies — to paste straight into Claude or any chat and pin down bugs across backend, app and firmware. A sticky capture notification (Android) and a built-in log browser (both platforms) keep it one tap away.

The name is a Naruto reference: the eye that sees everything.

30-second start (Android + Ktor)

// build.gradle.kts
dependencies {
debugImplementation("io.github.mibrahimdev:sharingan:0.1.1")
releaseImplementation("io.github.mibrahimdev:sharingan-noop:0.1.1")
}
// capture — one Ktor lineHttpClient { install(SharinganKtor) }
// open the log browser (or just tap the capture notification)Sharingan.show(context)

No init code — a manifest-merged ContentProvider starts Sharingan automatically. iOS, KMP and pure-Swift setup in Setup below.

Android

Sharingan home — HTTP tab, dark mode, with 401 and 500 failure railsDetail screen of a 500 error — summary cards, timing waterfall, headers, syntax-colored JSON bodyShare sheet with Copy for AI agent as the primary action
HTTP tab — live list with failure railsEvent detail — timing, headers, JSONShare sheet — "Copy for AI agent" first
MQTT tab — publish, receive and subscribe rows with QoS badges and failure railsBluetooth tab — GATT connect, discover, read, notify and error operationsSharingan home — HTTP tab in light mode
MQTT tab — pub/recv/sub with QoSBluetooth tab — GATT operationsLight mode follows the system theme
iOS — the same browser, presented from Swift via SharinganViewControllerKt.presentSharingan(animated:) (tap to expand)
Sharingan on iOS — HTTP tab, dark mode, with 401 and 500 failure railsiOS detail screen of a 500 error — summary cards, timing waterfall, headers, syntax-colored JSON bodyiOS share sheet with Copy for AI agent as the primary action
HTTP tab — live list with failure railsEvent detail — timing, headers, JSONShare sheet — "Copy for AI agent" first
iOS MQTT tab — publish, receive and subscribe rows with QoS badges and a failed broker statusiOS Bluetooth tab — GATT connect, discover, read, notify and error operationsSharingan on iOS — HTTP tab in light mode
MQTT tab — pub/recv/sub with QoSBluetooth tab — GATT operationsLight mode follows the system theme

Features

  • Three protocol tabs — HTTP, MQTT, Bluetooth (BLE/GATT), each with live counts, search and quick filters, in a compact terminal-density list.
  • Detail view per event — timing waterfall, request/response headers (secrets redacted at capture), syntax-colored JSON bodies, QoS/retain flags, GATT operations and decoded values.
  • Share sheet built for agents — "Copy for AI agent" produces structured Markdown an LLM parses reliably; cURL, raw JSON and the system share sheet are one tap away.
  • Zero release impact — swap in the sharingan-noop artifact: identical API, captures nothing, ships no UI.
  • Memory-only — an in-memory ring buffer (default 300 events); nothing is ever written to disk.
  • Minimal — no image assets, no icon fonts, no bundled typefaces, no DI, no navigation library. The capture core depends only on coroutines (+ Ktor when you use the plugin).

Requirements: Android API 24+, iOS arm64 + simulator arm64, Ktor 3.x for the HTTP plugin.

Setup

Get the artifacts

Sharingan is on Maven Central. Add mavenCentral() to your repositories (settings.gradle.ktsdependencyResolutionManagement { repositories { mavenCentral(); ... } }), then depend on the coordinate for your build type:

CoordinateUse
io.github.mibrahimdev:sharingan:0.1.1debug builds (capture + UI)
io.github.mibrahimdev:sharingan-noop:0.1.1release builds (same API, inert, no UI)

Tested versions: Sharingan 0.1.1 → Kotlin 2.4.0, Ktor 3.5.0, Compose Multiplatform 1.11.1, AGP 8.13.2. Later versions may work but are unverified — Kotlin/Native has no cross-compiler-version binary compatibility guarantee, so match the Kotlin version exactly.

Android app (two lines)

dependencies {
debugImplementation("io.github.mibrahimdev:sharingan:0.1.1")
releaseImplementation("io.github.mibrahimdev:sharingan-noop:0.1.1")
}

That's the whole integration story for build types: debug builds get capture + UI, release builds get the inert no-op (see what "no effect" means). On Android there is zero init code — Sharingan starts via a manifest-merged ContentProvider; no Application changes.

Kotlin Multiplatform app (shared module + iOS)

Gradle resolves one dependency list per source set, so pick the artifact with a property — and note both iOS requirements: the dependency must be apiin iosMain (not commonMain), and the framework block must export(...) it. Without both, Kotlin/Native generates an empty header and your Swift code won't see Sharingan at all.

// shared/build.gradle.kts
kotlin {
val sharinganArtifact =if (providers.gradleProperty("release").isPresent)
"io.github.mibrahimdev:sharingan-noop:0.1.1"else"io.github.mibrahimdev:sharingan:0.1.1"listOf(iosArm64(), iosSimulatorArm64()).forEach { target ->
target.binaries.framework {
baseName ="shared"
export(sharinganArtifact) // surfaces Sharingan in shared.h
}
}
sourceSets {
commonMain.dependencies {
implementation(sharinganArtifact)
}
iosMain.dependencies {
api(sharinganArtifact) // export() requires api at THIS source set
}
}
}

⚠️The release property must reach every build that produces the framework Xcode links — including the Gradle invocation inside your Xcode build phase (./gradlew :shared:embedAndSignAppleFrameworkForXcode -Prelease). If the flag is missing there, a debug framework silently overwrites your noop one.

Pure Swift app (no Kotlin, XCFramework)

./gradlew :sharingan:assembleSharinganReleaseXCFramework # debug-tool build
./gradlew :sharingan-noop:assembleSharinganReleaseXCFramework # inert twin

Outputs land in sharingan/build/XCFrameworks/release/Sharingan.xcframework and sharingan-noop/build/XCFrameworks/release/Sharingan.xcframework — same framework name on purpose, so import Sharingan compiles in every configuration and your build settings decide which one links:

Step-by-step Xcode wiring (search paths, embed, link)
  1. Copy both, e.g. Vendor/Debug/Sharingan.xcframework and Vendor/Release/Sharingan.xcframework.
  2. Add one of them to the target (General → Frameworks → Embed & Sign), then make both the search path and the embed input configuration-dependent: set FRAMEWORK_SEARCH_PATHS = $(SRCROOT)/Vendor/Debug for Debug and …/Release for Release, and reference the framework in the Embed Frameworks phase via $(FRAMEWORK_SEARCH_PATHS). Linking one variant but embedding the other dyld-crashes at launch — verify the embedded framework in the built .app matches the configuration.
  3. import Sharingan, then SharinganViewControllerKt.presentSharingan(animated: true).

A pure-Swift app has no Ktor plugin, so capture your URLSession traffic manually — call Sharingan.shared.http.log(...) from your networking layer (see Capturing traffic → HTTP for the manual-logging note and AGENTS.md for the full parameter list). Without it the viewer opens but stays empty. Heads-up: Kotlin default arguments don't bridge to Swift — every log(...) parameter is required; see the RECIPES.md gotcha before wiring it.

Don't mix this with the Maven/KMP path in one app — pick one.

Capturing traffic

HTTP — automatic (Ktor)

val client =HttpClient {
install(SharinganKtor) // that's it
}

The plugin records:

  • method, URL, status, headers, duration
  • textual bodies, capped at 64 KB (maxBodyBytes, configurable)
  • a TTFB / Download timing split

Authorization, Cookie, Set-Cookie and Proxy-Authorization values are masked at capture time — secrets never reach the buffer. Streaming responses (text/event-stream, binary) are never consumed. Transport failures are recorded, then rethrown untouched.

install(SharinganKtor) {
captureBodies =true// default — request/response bodies are recorded
maxBodyBytes =64*1024// default// overriding REPLACES the default set — re-list the defaults you want to keep
redactedHeaders =setOf("Authorization", "Proxy-Authorization", "Cookie", "Set-Cookie", "X-Api-Key")
}

Body capture is ON by default. To capture only metadata (method, URL, status, headers, timing) and never store body text, set captureBodies = false. To start paused and capture nothing at all until you flip it on — e.g. so a QA build records only the flow you ask for — call Sharingan.setRecording(false) at startup, then Sharingan.setRecording(true) (or tap Pause/Resume on the Android capture notification) when you're ready. Pausing applies to every protocol, not just HTTP.

Using OkHttp/NSURLSession directly? Log manually with Sharingan.http.log(...).

MQTT & BLE — one line per client callback

Sharingan is client-agnostic: wire its loggers into whatever MQTT/BLE library you use.

// MQTT (any client)Sharingan.mqtt.publish(topic, payloadAsText, qos =1, retained =false)
Sharingan.mqtt.received(topic, payloadAsText, qos =1)
Sharingan.mqtt.subscribed("devices/+/commands/#", qos =1)
// BLE (any GATT client — e.g. Kable)Sharingan.ble.connect(device ="HR-Monitor-9F")
Sharingan.ble.notify(device, characteristic ="Heart Rate Measurement", uuid ="0x2A37", value = decodedJson)
Sharingan.ble.read(device, characteristic, uuid, value)
Sharingan.ble.error(device, message ="Attribute not found (GATT 0x0A)", characteristic, uuid)

Payloads that parse as JSON get pretty-printing and syntax colors in the UI and in agent exports — log decoded JSON text whenever you can.

Kable adapter recipe
peripheral.observe(characteristic).onEach { bytes ->Sharingan.ble.notify(
device = peripheral.name ?: peripheral.identifier.toString(),
characteristic ="Heart Rate Measurement",
uuid = characteristic.characteristicUuid.toString(),
value = decodeHeartRate(bytes), // your decoder; JSON renders best
)
}.launchIn(scope)

More adapters (KMQTT/HiveMQ/Paho callbacks, full Kable wiring, shake-to-open) live in docs/RECIPES.md.

Opening the log browser

PlatformEntry point
AndroidTap the capture notification (appears on first event), or Sharingan.show(context)
iOSSharinganViewControllerKt.presentSharingan(animated: true) (one call, any thread), or embed SharinganViewControllerKt.SharinganViewController() yourself

Android

The capture notification shows per-protocol counters, a three-event ticker when expanded, and a Pause/Resume action. It is silent and updated in place. Two things to know:

  • Android 13+: the notification needs the POST_NOTIFICATIONS runtime permission.
    • Sharingan's AndroidManifest.xml declares it, so manifest merger surfaces it in your app's merged manifest (visible in Merged Manifest in Android Studio and in the APK).
    • The library never requests it at runtime — no requestPermissions call anywhere in Sharingan, so it can't trigger a prompt or change your app's runtime behavior; your app decides whether to ask the user.
    • Without the grant, capture still works silently — open the browser with Sharingan.show(context).
    • Ships only in debug/QA builds, so the permission never reaches your release manifest (the sharingan-noop release artifact declares nothing).
  • Do Not Disturb: because the notification is silent, DND hides it on most devices — Sharingan.show(context) always works (wire it to a debug-drawer button or shake gesture).

iOS

iOS has no sticky-notification equivalent; the view controller is the platform-conventional entry. Everything else — capture API, screens, share sheet — behaves identically on both platforms.

import SwiftUI
import shared // your shared framework
// One-call presentation (topmost view controller, any thread):
SharinganViewControllerKt.presentSharingan(animated:true)
// Or embed/present the view controller yourself:
structSharinganView:UIViewControllerRepresentable{func makeUIViewController(context:Context)->UIViewController{SharinganViewControllerKt.SharinganViewController()}func updateUIViewController(_ vc:UIViewController, context:Context){}}.sheet(isPresented: $showLogs){SharinganView()}

Two iOS notes:

  • No plist key needed. Compose Multiplatform 1.11 crashes host apps whose Info.plist lacks CADisableMinimumFrameDurationOnPhone; Sharingan disables that strict check so the viewer can never take your app down. Adding the key yourself is still worthwhile on ProMotion devices — it unlocks 120 Hz.
  • Keep your shared module lean. If you only capture HTTP (no custom debug UI), you do not need any Compose Multiplatform dependencies of your own — Sharingan brings its UI with it.

Sharing & exporting

The share sheet's primary action is Copy for AI agent: structured Markdown with method/path/status/host, request headers and the response body fenced as ```json — paste it straight into Claude or any chat. Every formatter behind the sheet is public API, so scripts and agents can produce identical output:

SharinganExport.agentMarkdown(event) // one event as structured MarkdownSharinganExport.agentMarkdown(events) // whole session, with counts headerSharinganExport.curl(httpEvent) // reproducible curl command (redactions stay masked)SharinganExport.json(event) // machine-readable single eventSharinganExport.sessionJson(events) // full session with tool metadataSharinganExport.summary(events) // human digest, one line per event

Debugging with a user? Ask them: open Sharingan → (optionally open the failing event) → Share → Copy for AI agent → paste.

Control surface

Sharingan.events // StateFlow<List<SharinganEvent>>, oldest firstSharingan.isRecording // StateFlow<Boolean>Sharingan.setRecording(false) // pause capture (REC/PAUSED toggle)Sharingan.clear() // drop everythingSharingan.setNotificationEnabled(false) // Android: opt out of the notification

SharinganStore(capacity) overrides the default 300-event ring (memory-only; process death clears it). Loggers are thread-safe, callable from any thread.

Release builds: what "no effect" means

  • sharingan-noop mirrors the public API: facade, loggers, SharinganKtor (installs, adds no hooks), Sharingan.show (no-op), SharinganViewController() (empty controller).
  • No Compose UI, no resources, no notification, no capture, no disk or network access in the artifact.
  • Event model classes stay real, so code that constructs or pattern-matches events behaves identically.
  • One caveat: SharinganScreen() (the embeddable composable) exists only in the debug artifact — embed it behind your own debug flag, or stick to the platform entry points above.

Sample app

sample/composeApp generates the design's IoT scenario offline (MockEngine): device state calls, a 401 token refresh, a 500 stream timeout, MQTT telemetry with a broker failure, and a BLE heart-rate session. All screenshots above come from it.

./gradlew :sample:composeApp:installDebug # Android
./gradlew :sample:composeApp:assembleRelease -Psharingan.noop # parity proof

Contributing & releases

To build against local changes, publish to your Maven local repo and add mavenLocal() to your repositories (settings.gradle.ktsdependencyResolutionManagement { repositories { mavenLocal(); ... } }):

git clone https://github.com/mibrahimdev/Sharingan &&cd Sharingan
./gradlew publishToMavenLocal

Contributors: :sharingan and :sharingan-noop must expose an identical public API so the debug→release swap is safe. ./gradlew checkApiParity (run in CI) enforces this and fails on any drift — see docs/api-parity.md for the contract and why a few debug-only symbols are excluded. After an intentional API change, run ./gradlew apiDump and commit the regenerated dumps.

Maintainers: cutting a Maven Central release is a two-step, stage-then-manually-release flow — see docs/RELEASING.md.

Documentation

DocWhat's in it
AGENTS.md (mirrored at llms.txt)Terse, complete API reference designed for machine consumption — point your AI agent here
docs/RECIPES.mdIntegration recipes: MQTT clients, Kable, SwiftUI wrapper, Live Activity analog, shake-to-open
docs/ARCHITECTURE.mdHow the library is built: module layout, capture pipeline, UI structure
docs/ROADMAP.mdFuture enhancements, recorded but not scheduled
docs/api-parity.mdWhy the debug/release artifacts must stay signature-identical and how checkApiParity enforces it
docs/RELEASING.mdStage-then-manually-release flow to Maven Central

License

Apache-2.0

About

Live HTTP · MQTT · Bluetooth logging for Kotlin Multiplatform — iOS & Android, one API

Resources

Code of conduct

Stars

25 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages