Builds and environments
AppInspect never sees your Gradle variant names. staging,
qa, uat, preprod, nightly —
all the same to the library. It decides whether to run from two inputs only, which
is why the answer to “will it work in my environment?” is short once you
know what they are.
Two inputs, three tiers
The first input is FLAG_DEBUGGABLE, which the Android Gradle plugin sets
from your build type's isDebuggable. The second is
appinspect_enabled_in_non_debuggable_build, a boolean resource you may
declare for one variant. Together they resolve to a tier.
FLAG_DEBUGGABLE | Opt-in resource | Tier | What happens |
|---|---|---|---|
| set | not read | DEBUG | Everything enabled. |
| clear | true | STAGING | Everything enabled. |
| clear | false or absent (the default) | PRODUCTION | Everything disabled. Nothing captured, nothing written, no way in. |
Note what the third row means in practice: the safe outcome is the one you get by doing nothing. A build has to be told to run the inspector, never told to stop.
Your setup, and what happens in it
| Your setup | Tier | Result | What you have to do |
|---|---|---|---|
debugImplementation of the full library |
DEBUG |
Works fully | Nothing. |
Custom variant, isDebuggable = true |
DEBUG |
Works fully | Nothing. The resource is never even read. |
Custom variant, isDebuggable = false |
PRODUCTION |
Dead — the inspector will not open | Add the opt-in resource, or make the variant debuggable. |
| The same, plus the opt-in resource | STAGING |
Works fully | Done. |
releaseImplementation of appinspect-no-op |
— | Inert by design; no inspection code in the APK | Nothing. This is the recommended release setup. |
Full library in release by mistake, no flags |
PRODUCTION |
Inert. Nothing breaks and nothing is captured | Still switch release to the no-op. |
Full library in release with the flag set |
STAGING |
It runs, in production | Don't. See the one way to shoot yourself. |
| Product flavours rather than build types | Follows the resolved variant's isDebuggable |
Works | Put the resource in src/<flavour>/res/. |
| A minified, R8'd, resource-shrunk non-debug variant | As above | Works fully | Nothing — see minification. |
The only decision you have to make
Is your non-debug variant isDebuggable? Open the build type and look,
remembering that initWith copies the value it inherits:
create("staging") {
initWith(getByName("release")) // inherits isDebuggable = false
isDebuggable = true // ← if this line is here, you are done
}
That pattern is common: teams inherit minification and signing from
release, then turn debuggability back on so the build stays attachable
to a debugger and to Studio's profilers. If your variant looks like that, AppInspect
already works there with no configuration and nothing on this page applies to you.
Turning it on in a non-debuggable build
Only if isDebuggable stays false do you need the opt-in.
Two equivalent ways — pick whichever fits how your project is organised.
create("uat") {
initWith(getByName("release"))
matchingFallbacks += listOf("release", "debug")
resValue("bool", "appinspect_enabled_in_non_debuggable_build", "true")
resValue("string", "appinspect_environment_name", "uat") // optional label
}
<resources>
<bool name="appinspect_enabled_in_non_debuggable_build">true</bool>
</resources>
Either one resolves that variant to STAGING with every feature enabled
and no host code at all.
appinspect_environment_name is optional and only labels the environment
in the Runtime panel — useful precisely because the
library cannot see that you called the variant uat.
Any custom build type needs it, because AppInspect publishes only
debug and release variants. Leave it out and Gradle
fails with “No matching variant of
io.github.suryansh1720001.appinspect:appinspect was found” before the
library gets a say in anything.
Why a resource and not a configuration flag
Two constraints, and nothing else satisfies both. Timing: AppInspect
installs itself from AndroidX Startup, which runs in a ContentProvider
before your Application.onCreate() — anything written in Kotlin
arrives after the enablement decision has been made, but a resource is readable at
that moment. Non-leakage: Android never merges one variant's
resources into another, so a value declared in uat is physically absent
from the release APK, which then gets the library's false default. A
BuildConfig field or a manifest placeholder cannot promise that.
It also cannot be inferred. At runtime a non-debuggable staging build and a release
build are byte-for-byte identical, and the fact that you scoped the dependency to
uatImplementation is Gradle information that does not survive into the
APK.
If you need to test on a release-like build
This comes up on every team: something only reproduces in a signed, minified, production-shaped build, and you want the inspector there for an afternoon. Do it with a variant, not with your release build.
- Create a variant that
initWith(release), so it keeps release's minification, shrinking and signing config, and give itmatchingFallbacks. - Either set
isDebuggable = trueon it, or add the opt-in resource to it. Both give you the full inspector; the resource keeps the build non-debuggable, which is closer to production. - Point the dependency at that variant:
add("stagingImplementation", …). Leavereleaseon the no-op. - Turn mocking off for that build —
allowResponseMocking = false— if it will ever point at production data. A build that can rewrite live responses is a support incident waiting to happen.
What you must not do is put the flag in release itself or in
src/main/res/, which applies to every variant. That is the one path
that ends with the inspector in front of real users.
The one way to shoot yourself
Write the opt-in into your release build type, or into
src/main/res/, while the full library is on the release classpath, and
the inspector will run in production. That is intentional and
unavoidable: it is an explicit instruction, in your own build script, in a line a
reviewer can see — exactly the trust level of typing
implementation instead of debugImplementation. No library
can defend against its owner deliberately enabling it.
Two rules make it unreachable in practice. Never put the resource in
src/main/res/ — use a build-type resValue or a variant
source set. And keep releaseImplementation(appinspect-no-op):
then there is no inspection code in the APK for any flag to enable, whatever the
flag says.
If it does happen, everything becomes active at once — capture writes real user
traffic including Authorization headers to the device, the crash handler
installs, the launcher shortcut publishes, notifications post, Logcat capture becomes
reachable, and response mocking can serve canned data to real users. The
security model spells out what to switch off
first, and note that the resource still cannot reach the
PRODUCTION tier: that additionally requires
allowInProductionBuilds = true, which is host Kotlin code that will not
even compile against the no-op.
Minification, R8 and resource shrinking
You add nothing. AppInspect ships its own
consumerProguardFiles, which Gradle applies to your app automatically,
so a variant created with initWith(getByName("release")) inherits
isMinifyEnabled and isShrinkResources and works unchanged.
The rules exist because the API layer reaches its storage, core and UI modules
through Class.forName — that is what lets any one of them be absent.
Every lookup has a graceful fallback, which is exactly why the keep rules ship with
the library rather than being left to you: a missing rule would not crash, it would
degrade quietly. The storage bridge going missing would drop you onto an in-memory
repository, so capture still works but nothing survives a restart; the core bridge
going missing would empty the Runtime panel's App, Device and Session sections. Both
read as feature bugs rather than build-configuration bugs.
The opt-in flag survives the resource shrinker too, because it is read as a
typed R.bool reference the shrinker can see rather than through
Resources.getIdentifier, which would have needed a
tools:keep rule in your app.
Where configuration fits
Everything above is build-time and decides whether AppInspect runs.
Everything on the configuration reference is
runtime and decides what it does once it is running — which panels
appear, what a tester can edit, what is masked. The two are independent, with one
connection worth knowing: AppInspect.install() replaces the whole
configuration, which discards the tier AppInspect inferred for the build, including
a staging variant's resource opt-in. Prefer
updateConfiguration, which keeps
it.