Filters, redaction & annotations¶
All configuration is plain Kotlin predicates — no annotations, no codegen.
Network (install(Snoop) { ... })¶
| Option | Type | Effect |
|---|---|---|
maxContentLength |
Long (default 250_000) |
Bodies larger than this are captured truncated, flagged in the UI. |
sanitizeHeader { name -> Bool } |
additive | Redact matching request/response headers. Authorization, Cookie, Set-Cookie are always redacted. |
sanitizeQueryParameter { name -> Bool } |
additive | Redact matching query parameters and form-body fields. A default list of secret-ish names is always redacted. |
sanitizeBody { text -> String } |
additive | Rewrite a captured body preview. Applied after the form-field redaction below. |
filter { request -> Bool } |
additive | Keep a request only if every filter returns true. A false skips capture entirely. |
install(Snoop) {
maxContentLength = 500_000
sanitizeHeader { it.equals("X-Api-Key", ignoreCase = true) }
filter { it.url.encodedPath != "/health" }
}
Network — native iOS (Snoop.installNetworkInterceptor)¶
The URLSession counterpart of the table above, for hosts on the Swift Package. Same engine, same
defaults; the differences are that every rule is a single closure rather than an additive list, and
that each call replaces the previous configuration instead of adding to it.
| Option | Type | Ktor counterpart |
|---|---|---|
maxContentLength |
Int64 (default 250_000) |
maxContentLength |
redactHeader |
((String) -> Bool)? |
sanitizeHeader |
redactQueryParameter |
((String) -> Bool)? |
sanitizeQueryParameter |
redactBody |
((String) -> String)? |
sanitizeBody |
keepRequest |
((Snoop.Request) -> Bool)? |
filter |
let configuration = URLSessionConfiguration.default
Snoop.installNetworkInterceptor(
on: configuration,
redactHeader: { $0.lowercased() == "x-api-key" },
keepRequest: { $0.path != "/health" }
)
Experimental, and partial by nature — see Native iOS for
what URLProtocol cannot reach.
Analytics (SnoopAnalytics { ... })¶
| Option | Type | Effect |
|---|---|---|
filter { event -> Bool } |
additive | Keep an event only if every filter returns true. |
sanitizeProperty { key -> Bool } |
additive | Redact matching property values. |
annotate { event -> List<Annotation> } |
additive | Attach semantic badges. |
groupBy(event, labelProperty) |
replaces | Which event opens a collapsible timeline group, and which property labels it. Defaults to "screen_view" / "screen_name". |
Annotation values: Ecommerce, Conversion, Consent, ScreenView.
val snoop = SnoopAnalytics {
filter { !it.name.startsWith("debug_") }
sanitizeProperty { it in setOf("user_email", "user_id") }
annotate {
buildList {
if (it.name in ecommerceEvents) add(Annotation.Ecommerce)
if (it.name == "purchase") add(Annotation.Conversion)
}
}
groupBy("page_shown", "page") // or groupBy(null) for no grouping at all
}
Grouping¶
The timeline collapses into sections: the event named in groupBy opens one, labelled with its
labelProperty value, and everything logged until the next one belongs to it. Out of the box that
is Firebase's manual screen event — screen_view, labelled with screen_name — so grouping works
without calling groupBy at all. When the event doesn't carry the property, the group takes the
event's own name; when the property is redacted, the label is the ██ placeholder.
The decision is made once, as the event is captured, and every viewer (embedded SnoopScreen,
the Android Activity, the web page, the native iOS hosts) groups by that stamp. Two consequences
worth knowing:
- Reconfiguring later only affects events captured from then on — it does not regroup the buffer.
- Each matching event opens a new group, so revisiting a screen shows up as a second section rather than merging into the first.
Grouping only exists where events are captured: a build without snoop-analytics has nothing to
group, and the viewers hide the Group chip rather than offering an action that can do nothing.
Redaction¶
Redacted values are replaced with a visible ██ placeholder, so a reviewer can confirm the field
was sent without exposing its value. Redaction happens at capture time — raw secrets never enter
the in-memory store, and the request that actually goes out is never touched.
Three things are redacted:
- Headers — the defaults (
authorization,cookie,set-cookie) plus yoursanitizeHeaderrules, matched against the name as sent and its lowercase form. - Query parameters — a built-in list of secret-ish names (
token,access_token,api_key,password,client_secret,signature, …) plus yoursanitizeQueryParameterrules. The URL fragment goes through the same rules, since an OAuth implicit-flow redirect hands the token back as#access_token=…; a fragment that isn't a parameter list (#section) is left alone. - Form bodies — a request body sent as
application/x-www-form-urlencodedis split onname=value&…and run through the same rules as query parameters, soPOST /oauth/tokenwithpassword=…&client_secret=…is stored aspassword=██&client_secret=██. Names are matched as sent: a percent-encoded name (client%5Fsecret) doesn't match. Requests only.
A predicate that throws counts as a match, so a broken rule fails closed and redacts.
Buffer size¶
The store keeps the most recent entries (default 500) in a ring buffer. Change it before traffic starts:
SnoopCore.configure(capacity = 1000)