DEV Community

Roronoa
Roronoa

Posted on

Keep First-Hour Inference Hosts Out of Android Bugreports

You sit with a Pixel 8 running Android 15, a React Native debug build, and a live voice eval still on screen. The first completion hangs, so you press Home, background the activity, and wait for the socket to die. A teammate asks for a bugreport zip, and you capture one because that feels like a normal first-hour debug move. That archive now holds the staging inference host you pointed at during onboarding, including paths you never meant to share.

Junior engineers hit this during the first hour more often than architecture reviews admit. You need a remote completion path because the on-device package is still gated behind another pull request. A debug host feels disposable until Android writes it into logcat, connectivity dumps, and crash breadcrumbs. The rest of this article is a proposed leak test, not a published benchmark with invented timings.

Why a first-hour host shows up in the zip

A debug inference URL is not only a Kotlin constant sitting in BuildConfig. OkHttp, Flipper, React Native networking, and your retry wrapper all echo that host when a stream stalls. Backgrounding a hung voice call is enough for the system to snapshot sockets, and adb bugreport keeps those lines. You also persist values that were supposed to live for a single morning.

SharedPreferences, MMKV, and AsyncStorage survive the session you treated as ephemeral. The first rollback then deletes the constant while the cached host remains on disk. Reviewers who only read the diff will miss that leftover. Treat the zip as part of the PR evidence, not as a side channel you share casually.

What you should treat as sensitive

  • The staging inference base URL and any path prefix that names the model route
  • Temporary session ids that bind a microphone buffer to that host
  • Debug bearer tokens pasted for hour one (never write these to logcat)
  • The product flavor that still compiles the remote fallback

Do not treat the host as harmless because it is not production. Bugreports leave the building when you file a ticket or drop a zip into chat. Play pre-launch reports and teammate laptops are also outside your debug device.

Proposed lab, not a measured claim

Record the environment before you change code. The setup below is a single-device experiment you can reproduce, not a claim that these exact builds were already timed.

  • Device: Pixel 8, or another Android 14+ phone you physically hold
  • OS: Android 15, developer options on, USB debugging authorized
  • App: React Native debug APK, microphone granted, unmetered Wi-Fi
  • Power: plugged in, battery optimization left at the package default
  • State: voice eval foregrounded, then Home to background the activity
  • Transition: hung completion → background → adb bugreport

Expected observation: the staging host appears under logcat or dumpsys inside bugreport-*.zip. The recovery outcome you want is stricter. After the flavor gate and the first rollback, a clean install plus a new bugreport must not contain that host.

Gate the host in a debug source set

Keep the remote fallback out of the release tree. A proposed Kotlin split looks like the following, and you should treat it as scaffolding rather than production networking.

// android/app/src/debug/java/com/example/inference/DebugInferenceHosts.kt
object DebugInferenceHosts {
    const val STAGING_BASE_URL = "https://staging.example.invalid/v1"
}

// android/app/src/release/java/com/example/inference/DebugInferenceHosts.kt
object DebugInferenceHosts {
    const val STAGING_BASE_URL: String? = null
}
Enter fullscreen mode Exit fullscreen mode
fun inferenceBaseUrl(): String {
    val debugHost = DebugInferenceHosts.STAGING_BASE_URL
    check(BuildConfig.DEBUG && debugHost != null) {
        "Remote fallback is debug-only; use the on-device path."
    }
    return debugHost
}
Enter fullscreen mode Exit fullscreen mode

Release compilation should fail closed if someone calls the remote path from shared code. You want a CI crash, not a silent production fallback that looks like an agent. React Native still needs the same fail-closed read on the JavaScript side.

// src/inference/host.ts
import { NativeModules } from "react-native";

export function getDebugInferenceHost(): string | null {
  if (!__DEV__) {
    return null;
  }
  return NativeModules.DebugInferenceHosts?.stagingBaseUrl ?? null;
}
Enter fullscreen mode Exit fullscreen mode

If __DEV__ is false, do not read AsyncStorage for a leftover URL. Juniors often restore a session on first launch and rehydrate the debug host into a release candidate. That rehydrate path is how a rollback looks complete in git and still fails on device.

Stop writing the host into logcat

OkHttp logging is the usual gift inside a bugreport. Keep verbose loggers out of the main source set, and install them only from debug code. BASIC still prints the host, which is fine on a private phone and not fine in a zip you attach to a ticket.

val client = OkHttpClient.Builder()
    .apply {
        if (BuildConfig.DEBUG) {
            addInterceptor(HttpLoggingInterceptor().apply {
                level = HttpLoggingInterceptor.Level.BASIC
                redactHeader("Authorization")
            })
        }
    }
    .build()
Enter fullscreen mode Exit fullscreen mode

For a hung voice call, log a local correlation id only. Never interpolate request.url. Never put the URL in Crashlytics keys, breadcrumb strings, or analytics events that survive process death.

Log.w("VoiceEval", "completion stalled; localSession=$localSessionId")
Enter fullscreen mode Exit fullscreen mode

If your interceptor pretty-prints JSON bodies, disable it before you reproduce the hang. Body loggers will also retain prompt fragments, which is a separate leak from the host itself.

Reproducible bugreport grep

After the background hang, search the archive before you file anything. The commands below are the artifact; run them on the same device you used for the voice eval.

adb bugreport ./bugreport-first-hour.zip
mkdir -p /tmp/br && unzip -q ./bugreport-first-hour.zip -d /tmp/br

HOST="staging.example.invalid"
rg -n "$HOST" /tmp/br || echo "host not found in bugreport"

# Fail the first PR if release dex still contains the host
unzip -p app/build/outputs/apk/release/app-release.apk classes*.dex \
  | strings \
  | rg -n "$HOST" \
  && echo "FAIL: host survived release dex" \
  || echo "release dex is clean"
Enter fullscreen mode Exit fullscreen mode

Pull preferences as well, because a clean dex does not mean a clean device.

adb shell run-as com.example.app \
  cat shared_prefs/inference.xml || true

adb shell run-as com.example.app ls files || true
Enter fullscreen mode Exit fullscreen mode

If the XML still holds the host after you removed the constant, the first rollback is incomplete. Repeat the grep after uninstall and reinstall, not only after assembleRelease.

Lifecycle checks before you open the PR

Run these in order on one device. Record pass or fail, not impressions. Keep microphone state, network state, and power state in the PR body beside the diff.

  1. Permission revoke. Open Settings → Apps → your app → Microphone → Don't allow, then resume the eval. The client should stop capture and must not dump the last request URL.
  2. Backgrounding. With a live stream, press Home and wait thirty seconds. Resume and confirm you only logged localSessionId.
  3. Process death. Run adb shell am kill com.example.app, relaunch, and confirm storage did not restore the debug host on a release-shaped build.
  4. First rollback. Delete the debug host from the debug source set, assemble release, then grep dex and a fresh bugreport.

Decision table for hour one

Condition Remote host allowed? What you still grep
Debug flavor, mic granted, first hour Staging only bugreport zip, logcat, prefs
Debug flavor, mic revoked None; stop capture last request logs
Release candidate None APK strings, crash keys
After rollback tag None device prefs, leftover MMKV files

The table is a checklist, not a claim that every OEM dump looks identical. If a line fails, block the PR even when the feature demo still talks.

Where a throwaway host belongs

Hour one still needs somewhere to send a test completion when the on-device model is not packaged. Point the debug flavor at a host you control, not at production inference, and not at a laptop that will sleep during review. Disclosure: This article was prepared as part of MonkeyCode's product outreach. MonkeyCode offers free model access and a free server option you can use as that disposable staging target while you prove the flavor gate, without borrowing production credentials. It does not replace on-device packaging, permission handling, or the grep steps, and this article does not claim model names, quotas, hardware, or uptime.

Limitations and who should skip this

This approach is debug-flavor scaffolding for a junior joining a mobile AI repo. It is the wrong path for customer audio, paid production inference, or any SKU that must stay on-device. Healthcare, offline-only catalogs, and anything covered by a signed model license should not use a free staging box as a fallback.

strings on dex can miss split URLs and obfuscated concatenation, so a clean grep is necessary rather than sufficient. iOS sysdiagnose is a different artifact, and you should not assume the Android zip search transfers. This article also skips energy numbers, because a leak test is not a battery benchmark.

First rollback, then open the PR

Your first PR should include the flavor files, the fail-closed React Native read, the log redaction, and a note of the bugreport grep. The description should record device, OS, permission state, network state, and whether the hung session recovered, restarted, or silently disappeared. After merge, run the rollback on a clean install once. If the host returns from disk, onboarding is not done; only the diff is.

If you run the same transition, reply with device, OS, the background and revoke steps, and whether the host recovered, restarted, or vanished from the next bugreport.

Top comments (0)