Skip to content

Getting started

Install

Use the real artifacts in debug and the no-op mirrors in release, so release builds capture nothing and drop the UI/body-retention overhead:

dependencies {
    debugImplementation("io.github.asanre:snoop-ktor:0.3.0")
    releaseImplementation("io.github.asanre:snoop-ktor-no-op:0.3.0")

    // the on-device inspector UI:
    debugImplementation("io.github.asanre:snoop-ui:0.3.0")
    releaseImplementation("io.github.asanre:snoop-ui-no-op:0.3.0")

    // optional, only if you also want analytics inspection:
    debugImplementation("io.github.asanre:snoop-analytics:0.3.0")
    releaseImplementation("io.github.asanre:snoop-analytics-no-op:0.3.0")
}

snoop-core (models, store, redaction and the web viewer) arrives transitively — you never declare it. The no-op mirrors declare the exact same classes and functions, so your call sites compile unchanged.

Skipping snoop-ui

snoop-ui is the only artifact that pulls in Compose. Leave it out if you inspect from the web viewer instead of on-device — everything else keeps working, and Compose never enters your dependency graph.

Capture network traffic

Install the plugin on your Ktor client:

val client = HttpClient(engine) {
    install(Snoop) {
        maxContentLength = 250_000                              // truncate bodies larger than this
        sanitizeHeader { it == HttpHeaders.Authorization }      // Authorization/Cookie/Set-Cookie are redacted by default
        sanitizeQueryParameter { it == "token" }               // common secret params are redacted by default
        sanitizeBody { it.replace(emailRegex, "██") }          // rewrite the stored preview only
        filter { request -> request.url.host != "metrics.internal" }
    }
}

Every option is a plain predicate — see Filters, redaction & annotations for the full list.

Capture analytics events

Snoop never talks to an analytics SDK. Wire your own analytics facade to log:

val snoop = SnoopAnalytics {
    filter { event -> !event.name.startsWith("debug_") }
    sanitizeProperty { key -> key in setOf("user_email", "user_id") }
    annotate { event -> if (event.name in ecommerceEvents) listOf(Annotation.Ecommerce) else emptyList() }
}

// from your facade, alongside the real SDK call:
snoop.log("purchase", channel = "firebase", properties = mapOf("value" to 42.5))

See Hook your analytics facade for the pattern.

Tune retention

The store is an in-memory ring buffer with two drop-oldest limits: entry count and approximate bytes. Defaults are 500 entries and 20 MB. Reconfigure before traffic starts (it clears the store; the StateFlow survives, so an already-open inspector keeps working):

SnoopCore.configure(capacity = 1_000, maxTotalBytes = 40_000_000)

The same call renames the tool. title reaches every viewer — the inspector's header, the Android and iOS notifications, and the web page's tab and header — so a host that would rather ship its own name sets it once:

SnoopCore.configure(title = "Patata Inspector")

Unlike the limits, changing the title leaves the store alone. That covers every surface Snoop draws; the one name it cannot set is Android's task label, which is normally your app's anyway — see Where the Activity lands.

How the viewers group the timeline is set on the analytics logger instead — see Grouping.

Next: open the inspector.