Skip to content
Codeskop

OkHttp interceptors for network monitoring

Application vs network interceptors, HttpLoggingInterceptor pitfalls, EventListener timings, header redaction and measuring API failures on Android.

Nashon Olel

Founder, Codeskop

9 min read

okhttp-interceptor-network-monitoring

To monitor network calls in an Android app, add an application interceptor to record failed requests (4xx, 5xx, timeouts, connection errors) and an EventListener to record timings (DNS, connect, TLS, time to first byte, total). Use HttpLoggingInterceptor only in debug builds: at HEADERS or BODY level it can leak Authorization headers, cookies and request bodies into your logs. Whatever you record, strip query strings and redact credentials before the data leaves the device.

This guide shows each piece with Kotlin code, then how Codeskop's tracker-okhttp adapter does the same with one line. If you're working on app stability in general, read Android ANRs: causes, detection and fixes, because blocking on network calls is a classic ANR cause. The Codeskop side is in the network capture docs. Flutter teams using Dio can apply the same ideas; see Flutter error handling.

Application vs network interceptors

OkHttp lets you register an interceptor at two points. The OkHttp interceptors docs compare them:

Application interceptor (addInterceptor)Network interceptor (addNetworkInterceptor)
CalledOnce per call, even if the response comes from the cacheFor each network request, including redirects and retries; not for cached responses
SeesThe app's original request, without OkHttp-added headers such as Accept-Encoding: gzipThe request exactly as sent over the wire
Connectionchain.connection() is nullHas the Connection (IP address, TLS details)
Can short-circuit or retryYesNo (it must call proceed once)

For monitoring, use an application interceptor. It sees one outcome per logical call, including the final result after redirects and retries, which is what your users experience. Use a network interceptor when you need to see what actually went over the wire, for example to debug a redirect loop or check compression headers.

kotlin
val client = OkHttpClient.Builder()    .addInterceptor(FailureInterceptor())          // application: one outcome per call    .addNetworkInterceptor(WireDebugInterceptor()) // network: every hop, debug only    .build()

Catch crashes and ANRs in production

Add com.codeskop.sdk:tracker-android from Maven Central (minSdk 24). It starts on its own, with no init code required.

Android quick start

HttpLoggingInterceptor and its pitfalls

OkHttp's logging interceptor is the quickest way to see traffic. Keep its version in step with your OkHttp version (the BOM helps):

kotlin
dependencies {    implementation(platform("com.squareup.okhttp3:okhttp-bom:5.5.0"))    implementation("com.squareup.okhttp3:okhttp")    implementation("com.squareup.okhttp3:logging-interceptor")}

The levels are NONE, BASIC (request line and response status), HEADERS and BODY. OkHttp's own README warns that logs at HEADERS or BODY level "have the potential to leak sensitive information such as 'Authorization' or 'Cookie' headers and the contents of request and response bodies", and should only be used in a controlled way or outside production.

A safe configuration:

kotlin
val logging = HttpLoggingInterceptor().apply {    level = if (BuildConfig.DEBUG) {        HttpLoggingInterceptor.Level.BODY    } else {        HttpLoggingInterceptor.Level.NONE    }    redactHeader("Authorization")    redactHeader("Cookie")    redactHeader("Set-Cookie")}

The pitfalls we see most often:

  • Shipping BODY in release. Logcat ends up in bug reports and in any log-forwarding SDK. Phone numbers, OTPs, M-Pesa transaction details and tokens in request bodies all leak this way.
  • Forgetting custom auth headers. redactHeader only redacts what you name. If your API uses X-Api-Key or X-Session-Token, add them.
  • Tokens in query strings. redactHeader doesn't touch URLs. ?token=... is logged as-is at every level.
  • Using logs as monitoring. Logs on a user's phone never reach you. Logging is a debugging tool; monitoring means sending structured, redacted events to a server.

Recording failures with an application interceptor

This interceptor records every call that fails: a non-2xx response or an IOException (timeout, DNS failure, connection reset). It never changes the response, and it rethrows exceptions so your app behaves exactly as before.

kotlin
class FailureInterceptor(    private val recorder: NetworkRecorder,) : Interceptor {     override fun intercept(chain: Interceptor.Chain): Response {        val request = chain.request()        val started = System.nanoTime()        val path = request.url.encodedPath          // no query string         val response = try {            chain.proceed(request)        } catch (e: IOException) {            recorder.failure(request.method, request.url.host, path,                status = null, kind = errorKind(e), tookMs = elapsedMs(started))            throw e        }         if (!response.isSuccessful) {            recorder.failure(request.method, request.url.host, path,                status = response.code,                kind = if (response.code >= 500) "http_5xx" else "http_4xx",                tookMs = elapsedMs(started))        }        return response    }     private fun errorKind(e: IOException) = when (e) {        is SocketTimeoutException -> "timeout"        is UnknownHostException -> "dns"        is InterruptedIOException -> "timeout"   // includes call timeouts        else -> "network_error"    }     private fun elapsedMs(start: Long) = (System.nanoTime() - start) / 1_000_000}

Two details matter when you aggregate. First, use the path, not the full URL, and collapse IDs (/orders/42 → /orders/{id}) so failures group sensibly. Second, record a 3xx only if your client doesn't follow redirects. By default OkHttp follows them, and the application interceptor sees the final response.

Measuring latency with EventListener

An interceptor can time a whole call, but it can't tell you where the time went. EventListener receives callbacks for each phase: callStart, dnsStart/dnsEnd, connectStart, secureConnectStart/secureConnectEnd, connectEnd, connectionAcquired, the request and response header and body events, connectionReleased and finally callEnd, or callFailed if the call fails permanently.

Because calls run concurrently, create one listener per call with a Factory:

kotlin
class TimingListener(    private val call: Call,    private val sink: (CallTiming) -> Unit,) : EventListener() {    private val start = System.nanoTime()    private var dnsStart = 0L; private var dnsMs = -1L    private var connectStart = 0L; private var connectMs = -1L    private var tlsStart = 0L; private var tlsMs = -1L    private var ttfbMs = -1L     override fun dnsStart(call: Call, domainName: String) { dnsStart = System.nanoTime() }    override fun dnsEnd(call: Call, domainName: String, inetAddressList: List<InetAddress>) {        dnsMs = ms(dnsStart)    }    override fun connectStart(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy) {        connectStart = System.nanoTime()    }    override fun secureConnectStart(call: Call) { tlsStart = System.nanoTime() }    override fun secureConnectEnd(call: Call, handshake: Handshake?) { tlsMs = ms(tlsStart) }    override fun connectEnd(call: Call, inetSocketAddress: InetSocketAddress, proxy: Proxy, protocol: Protocol?) {        connectMs = ms(connectStart)    }    override fun responseHeadersStart(call: Call) { ttfbMs = ms(start) }    override fun callEnd(call: Call) = emit(failed = false)    override fun callFailed(call: Call, ioe: IOException) = emit(failed = true)     private fun emit(failed: Boolean) = sink(        CallTiming(call.request().method, call.request().url.encodedPath,            dnsMs, connectMs, tlsMs, ttfbMs, totalMs = ms(start), failed = failed)    )    private fun ms(from: Long) = (System.nanoTime() - from) / 1_000_000     class Factory(private val sink: (CallTiming) -> Unit) : EventListener.Factory {        override fun create(call: Call): EventListener = TimingListener(call, sink)    }} val client = OkHttpClient.Builder()    .addInterceptor(FailureInterceptor(recorder))    .eventListenerFactory(TimingListener.Factory { timing -> recorder.timing(timing) })    .build()

Things to know:

  • Pooled connections skip DNS, connect and TLS. The OkHttp docs show a second request to the same host firing only connectionAcquired and the request/response events. A -1 for those phases is normal.
  • Retries fire events more than once. connectFailed() isn't terminal if OkHttp recovers, so keep the last value or sum them, but be consistent.
  • Keep callbacks cheap. They run on OkHttp's threads, inside the call. Hand the data to a queue; don't do disk or network I/O in the callback.
  • One factory per client. eventListenerFactory takes a single factory. To combine several listeners, write a small delegating listener.
  • Sample timings, not failures. Every call produces a timing, so sample those (the docs show a factory returning EventListener.NONE for 90% of calls). Keep every failure.

Measuring failures and latency that matter

Once you have the data, a few views answer most questions:

  • Failure rate per endpoint (method + path), split by status class and error kind. A spike in timeout with no rise in http_5xx points at the network or a slow dependency, not a crashing server.
  • p50 and p95 total time per endpoint, and the phase breakdown for slow calls. High DNS and connect times with a normal TTFB is a network problem; high TTFB is a server problem.
  • Breakdown by app version, device and network type, so you can tell a bad release from a bad area.

This matters in markets like Kenya. Outside the main cities, 3G coverage is patchy and DNS and TLS setup on a congested cell can dominate total time. Payment flows that call the M-Pesa Daraja API can time out while waiting on a callback. A per-phase breakdown tells you whether to fix your server, your timeouts or your retry logic.

Note

Network route analysis results on Codeskop below

Screenshot 2026 10 08 at 15.54.08

Redacting headers and URLs

Before sending anything off the device:

  1. Drop the query string: request.url.newBuilder().query(null).build(), or just record encodedPath.
  2. Allow-list headers rather than block-listing them. Record Content-Type, Content-Length and Cache-Control; never Authorization, Cookie or Set-Cookie.
  3. Never record bodies in production monitoring. Record sizes instead (request.body?.contentLength(), Content-Length on the response).
  4. Collapse IDs in paths so user or order numbers don't become personal data in your metrics.

Network capture with Codeskop

If you'd rather not build and maintain this yourself, Codeskop's tracker-okhttp adapter registers both pieces, a failure interceptor and an event listener, with one line. Network capture is included on the Starter plan and above ($49/month as of October 2026; see pricing). On Free, the SDK doesn't capture network calls even with the adapter installed; upgrading switches it on without a new release.

kotlin
dependencies {    implementation("com.codeskop.sdk:tracker-android:0.3.0")    implementation("com.codeskop.sdk:tracker-okhttp:0.3.0") // optional: network capture}
kotlin
import com.codeskop.sdk.okhttp.codeskopimport okhttp3.OkHttpClient val client = OkHttpClient.Builder()    .codeskop()   // failed requests + timing for every request    .build()

OkHttp has no global hook, so adding the dependency alone captures nothing. Build the client your app actually uses (including the one you pass to Retrofit) with .codeskop(). If you already set your own eventListenerFactory, keep it and add just the interceptor:

kotlin
val client = OkHttpClient.Builder()    .addInterceptor(CodeskopOkHttpInterceptor())   // keep your own eventListenerFactory    .eventListenerFactory(myListenerFactory)    .build()

What it records, per the network capture docs:

  • Failures: any 4xx or 5xx response, timeout or connection failure, with error kinds http_4xx, http_5xx, timeout or network_error. Your exception still reaches your code unchanged.
  • Timings: DNS, connect, TLS, time to first byte and total for each completed request. Timings are sampled through remote config; failures are always kept.
  • Context: method, host and path, request and response sizes, and the device, app version and user.
  • Never captured: request and response bodies, query strings, and the Authorization, Cookie and Set-Cookie headers. Any header in redactHeaders is masked as a second safeguard.

Failed requests become issues grouped by method, path and status, with IDs collapsed (POST /orders/{id} → 500). Latency charts built from the timings are coming to the dashboard; failures are there today. Events are queued on the device and uploaded by WorkManager when there's a connection, so failures that happen because the user lost signal are still reported later.

Note

Codeskop installation docs list OkHttp 4 as the requirement for tracker-okhttp. This is the minimum supported version but will work perfectly with any later versions.

Screenshot 2026 10 08 at 15.58.41

Frequently asked questions

Should I use addInterceptor or addNetworkInterceptor for logging?

For monitoring, use addInterceptor: it sees one outcome per call, including cached responses. Use addNetworkInterceptor to debug what goes over the wire, such as redirects, retries and OkHttp-added headers.

Is it safe to use HttpLoggingInterceptor in production?

Only at NONE or BASIC. At HEADERS or BODY it can log authorization headers, cookies and request bodies. Set the level from BuildConfig.DEBUG and call redactHeader for every credential header.

How do I measure DNS and TLS time in OkHttp?

Use an EventListener with a Factory so each call gets its own instance, and time dnsStart→dnsEnd and secureConnectStart→secureConnectEnd. Pooled connections skip those phases.

Does an interceptor catch timeouts?

Yes. A timeout surfaces as an IOException (usually SocketTimeoutException or InterruptedIOException) from chain.proceed(). Catch it, record it, and rethrow.

Does Codeskop capture request bodies?

No. Codeskop never captures request or response bodies, strips query strings, and never collects Authorization, Cookie or Set-Cookie.

Know what broke. Fix it faster.

Crash and error reporting, with email alerts, is free for 10,000 events a month. ANR, API monitoring and Slack, WhatsApp and webhook alerts start on Starter, from $49 a month (as of October 2026), billed in USD.

build.gradle.kts
dependencies {    implementation("com.codeskop.sdk:tracker-android:0.3.0")    implementation("com.codeskop.sdk:tracker-okhttp:0.3.0") // optional: network capture}

Get new posts by email

New posts on mobile crash reporting, ANRs and API monitoring, about twice a month. Unsubscribe anytime.

OkHttp interceptors for network monitoring | Codeskop Blog