Filters, redaction & annotations¶
All configuration is plain Kotlin predicates — no annotations, no codegen.
Network (install(Verivent) { ... })¶
| 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. |
sanitizePath { path -> String } |
additive, off by default | Rewrite the captured path, percent-encoded as sent, in the path and the URL. |
filter { request -> Bool } |
additive | Keep a request only if every filter returns true. A false skips capture entirely. |
A body whose declared charset can't be used here is shown as UTF-8, with a warning in the log once per charset.
install(Verivent) {
maxContentLength = 500_000
sanitizeHeader { it.equals("X-Api-Key", ignoreCase = true) }
filter { it.url.encodedPath != "/health" }
}
Network — native iOS (Verivent.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 |
redactPath |
((String) -> String)? |
sanitizePath |
keepRequest |
((Verivent.Request) -> Bool)? |
filter |
let configuration = URLSessionConfiguration.default
Verivent.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 (AnalyticsSink { ... })¶
| Option | Type | Effect |
|---|---|---|
filter { event -> Bool } |
additive | Keep an event only if every filter returns true. |
sanitizeProperty { key -> Bool } |
additive | Redact matching property values, in the event and in every extra parameter block. |
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 sink = AnalyticsSink {
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 InspectorScreen,
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.
- A collapsed group stays collapsed when its opening event leaves the buffer.
Grouping only exists where events are captured: the viewers group the timeline as soon as an event
carries a group label, and show one flat list otherwise. The viewers have no switch of their own: to turn
grouping off, call groupBy(null) where events are captured.
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. -
Path segments, only if you ask — nothing in a path is redacted by default, since most of it (
/orders/123) is what you want to see. A path that carries a secret (/reset/<token>,/v1/keys/<key>) needs asanitizePath(redactPathon iOS) rule, which gets the path percent-encoded as sent on both platforms:sanitizePath { it.replace(Regex("^/reset/[^/]+"), "/reset/██") }
A predicate that throws counts as a match, so a broken rule fails closed and redacts; a path rule that throws hides the whole path.
Buffer size¶
The store keeps the most recent entries (default 500) in a ring buffer. Change it before traffic starts:
Inspector.configure(capacity = 1000)