getting started
Two Gradle lines.
Zero init code.
Sharingan ships as two artifacts with the same public API: the real one
for debug builds, an inert no-op for release. Pick per build type and you're done — on
Android, capture starts from a manifest-merged ContentProvider, so there is
nothing to call.
Get the artifacts
Sharingan is on Maven
Central. Add mavenCentral() to your repositories
(settings.gradle.kts →
dependencyResolutionManagement { repositories { mavenCentral(); … } }), then
depend on the coordinate for your build type:
io.github.mibrahimdev:sharingan:0.1.0 for debug (capture + UI) and
io.github.mibrahimdev:sharingan-noop:0.1.0 for release (same API, inert,
no UI).
Building from source / contributing
To build against local changes, publish to your Maven local repo and add
mavenLocal() to your repositories
(dependencyResolutionManagement { repositories { mavenLocal(); … } }):
git clone https://github.com/mibrahimdev/Sharingan && cd Sharingan
./gradlew publishToMavenLocal
Install — Android app
dependencies {
debugImplementation("io.github.mibrahimdev:sharingan:0.1.0")
releaseImplementation("io.github.mibrahimdev:sharingan-noop:0.1.0")
}
That is the whole build-type story: debug builds get capture + the log browser, release builds get the no-op (what "no effect" means).
Requirements: Android API 24+ · iOS arm64 + simulator arm64 · Ktor 3.x for the HTTP plugin.
Install — Kotlin Multiplatform (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 api in
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.
kotlin {
val sharinganArtifact = if (providers.gradleProperty("release").isPresent)
"io.github.mibrahimdev:sharingan-noop:0.1.0" else "io.github.mibrahimdev:sharingan:0.1.0"
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
}
}
}
-Prelease flag 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.Kotlin/Native dead-code elimination additionally strips whatever the no-op doesn't reference.
Install — Pure Swift app (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:
- Copy both, e.g.
Vendor/Debug/Sharingan.xcframeworkandVendor/Release/Sharingan.xcframework. - 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/Debugfor Debug and…/Releasefor Release. Reference the framework in the Embed Frameworks phase via$(FRAMEWORK_SEARCH_PATHS), and after building, verify the embedded framework inside the built.appmatches the configuration. Linking one variant but embedding the other dyld-crashes at launch. import Sharingan, thenSharinganViewControllerKt.presentSharingan(animated: true).
Don't mix this with the Maven/KMP path in one app — pick one.
Capture HTTP — the Ktor plugin
val client = HttpClient {
install(SharinganKtor) // that's it
}
The plugin records method, URL, status, headers, textual bodies (capped at 64 KB, configurable), duration, and a TTFB/Download timing split.
- Secrets are masked at capture time —
Authorization,Cookie,Set-Cookie,Proxy-Authorizationnever reach the buffer. - Streams are never consumed —
text/event-streamand binary bodies pass through untouched. - Failures are rethrown untouched — Sharingan records the transport error, then your error handling sees exactly what it would have seen.
Tuning, plus MQTT/BLE wiring and OkHttp guidance, live in Integrations.
Open the log browser
| Platform | Entry point |
|---|---|
| Android | Tap the capture notification (appears on first event), or call Sharingan.show(context) |
| iOS | SharinganViewControllerKt.presentSharingan(animated: true) (one call, any thread), or embed SharinganViewControllerKt.SharinganViewController() yourself |
The Android notification shows per-protocol counters, a three-event ticker when expanded, and a Pause/Resume action. It is silent and updates in place. Two platform realities to know:
POST_NOTIFICATIONS runtime permission — Sharingan declares it, your app
requests it. Without the grant capture still works; open the browser with
Sharingan.show(context).Sharingan.show(context) to a debug-drawer
button or shake gesture so the browser is always reachable.On iOS the view controller is the platform-conventional entry; everything else — capture API, screens, share sheet — behaves identically on both platforms.
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.A SwiftUI wrapper recipe is in Integrations.
Release safety — what "no effect" means
sharingan-noopmirrors 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 across artifacts.
- 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.
Prove parity in your own build: ./gradlew assembleRelease with the
no-op swapped in compiles against the identical API surface.
Control surface
Sharingan.events // StateFlow<List<SharinganEvent>>, oldest first
Sharingan.isRecording // StateFlow<Boolean>
Sharingan.setRecording(false) // pause capture (REC/PAUSED toggle)
Sharingan.clear() // drop everything
Sharingan.setNotificationEnabled(false) // Android: opt out of the notification
The buffer is an in-memory ring — default 300 events,
SharinganStore(capacity) for custom sizes. Nothing is ever written to
disk; process death clears it. Loggers are thread-safe and callable from any thread.