Quick Reference
Manual API calls (
identify, track, screen, triggerExperience) route to whichever instance you call them on — no extra wiring is needed.
UserpilotConfig.isDefault defaults to true. The host application does not need to set isDefault = true explicitly. Embedded vendor SDKs must set isDefault = false.
Event Routing
Every event — autocapture interaction, autocapture screen, or a manualscreen(_:) / track(_:) — routes to one Userpilot instance using a single three-tier rule:
- Explicit
userpilot:argument, when the API exposes one (for exampleUserpilotComposeNavigationTracker(..., userpilot = ...)). - Anchored owner, when something in the UI hierarchy claims the originating UI (
attachToActivity(...), orUserpilotOwner { … }/LocalUserpilotOwnerin Compose). - Default fallback — the instance that claimed
isDefault(defaults totrue).
isDefault defaults to true), so nothing in your integration changes.
Idempotent Factory
Userpilot(context, token) is a get-or-create factory. UserpilotConfig.isDefault defaults to true, so the first live instance that does not opt out claims the default role automatically. Subsequent calls with the same token return the existing instance:
isDefault at its default and tries to claim the role while another instance already holds it will not displace the existing claimant — the SDK logs a warning and un-anchored events keep routing to whoever already claimed the role.
Different tokens coexist as independent instances — each with its own socket, autocapture coordinator, and lifecycle callbacks.
The Default Instance
The SDK tracks the live instance that claimed the default role viaisDefault = true. This fallback is used for un-anchored autocapture and screen-tracker call sites.
Userpilot instance returned by the factory. In the single-instance case this is your only instance (isDefault defaults to true). In the multi-instance case the default role is typically held by the host app, which does not need to set isDefault = true explicitly. Resolution is claim-based, not order-based, so init order between correctly configured tenants does not matter.
Embedded vendor SDKs must opt out:
isDefault = true (the default) claims that role on registration when the role is unclaimed. If the role is already held, the new claim is rejected (a warning is logged) and the existing claimant keeps the role.
This is claim-based, not registration-order-based: registering first does not make an instance the default unless it also opts in with isDefault = true, and there is no first-registered fallback when every instance opts out.
Correct setup: the host leaves isDefault at its default (true); every embedded vendor sets isDefault = false. The host then holds the default role regardless of init order — a vendor that opts out never competes for the slot even if it initializes first.
Misconfiguration: if two instances both leave isDefault = true, whichever registers while the role is still unclaimed becomes the default; any later conflicting claim is rejected. Do not rely on init order — always set isDefault = false on embedded instances.
If no instance claims the default role (every instance set isDefault = false), there is no SDK default fallback and un-anchored events are dropped. Keep isDefault at its default (true) on the host app.
Host app + embedded vendor
Once a vendor SDK opts out and anchors its own UI, the host app does nothing special for default resolution:Compose Routing
Wrap each Compose subtree inUserpilotOwner(yourUserpilot) { … }. Nested wrappers shadow outer ones, so an embedded SDK can take ownership of its own UI even when the host declares a different owner at the root.
UserpilotComposeNavigationTracker also accepts an explicit userpilot: argument:
- The
userpilot:argument if non-null. LocalUserpilotOwner.currentif aUserpilotOwneris in scope.- The SDK default fallback.
Modifier.userpilotScreen(...) resolves via LocalUserpilotOwner → SDK default fallback.
View Routing
For Android Views, ownership is anchored on the hosting Activity’s decor view. CallattachToActivity from each owning Activity’s onCreate:
Interaction events
Click, value-change, and text-input handlers resolve the owner via:- Parent-chain walk looking for the
userpilot_owner_tagset byattachToActivity. - Context fallback — unwrap
View.contextto the hosting Activity and read the tag from its decor view (for example Dialog clicks). - Default fallback — the SDK default fallback handles the event.
Dialog.show(), so subsequent inner clicks resolve cheaply via the parent chain.
Screen events
Each instance installs its own lifecycle callbacks, but a per-tracker gate ensures only one tenant publishes a screen event per Activity or Fragment:
With multiple tenants registered, an un-anchored Activity emits exactly one screen event on the default — not one copy per tenant.
Forwarding Events to the Host
By default, autocapture events publish only on the instance that owns the originating UI. A vendor-owned screen or click is reported to the vendor tenant only. If the host app wants autocapture events that originate inside embedded vendor SDKs, opt in on the host (default) instance:- Autocapture events that resolve to a non-default instance are published to that instance and forwarded to the default instance.
- The flag is read only on the resolved default instance. Setting it on a vendor instance has no effect.
- Forwarded events are delivered unchanged through the host’s publisher and
UserpilotAnalyticsListener. There is no extra tagging. - The host’s own events are never duplicated to itself, and forwarding never re-routes, so there is no fan-out loop.
Gradle Plugin Scope
The Userpilot Gradle plugin instruments bytecode at build time. By default it usesInstrumentationScope.ALL, which transforms your module plus every dependency JAR/AAR — correct for end-user apps.
If you ship an SDK embedded in someone else’s app, restrict instrumentation to your module:
PROJECT (case-insensitive) restricts class-visitor transformation to the current module’s compiled classes. The embedding app can apply its own plugin configuration independently. Any other value keeps the default ALL behavior.
Routing Matrix
Each instance owns its own capture config (
enableInteractionAutoCapture, enableInteractionTextCapture, enableInteractionAccessibilityLabelCapture, enableInteractionValueCapture), so two tenants can hold different autocapture preferences in the same process.
Limitations
- View-less callbacks cannot resolve an anchored owner. They always route to the SDK default fallback (the
isDefaultclaimant), or are dropped when no instance holds that role. - Same-process re-init with the same token returns the existing instance and discards the DSL block on the second call.
- Activity screen events publish once per Activity or Fragment — to whichever tenant owns the anchor, or to the default when un-anchored. If both tenants need the full session, re-emit the screen manually with
instance.screen("…").