Analytics
Native analytics on the android channel — sink, customer adapters, and the onAnalyticsEvent callback.
On mobile, analytics flow through the SDK — one integration gives you chat and
provider-agnostic analytics that merge with your web reports. The web runtime emits
the canonical events; the SDK re-dispatches them natively with
channel: "android", so web and app reports share one taxonomy.
Where events go
LoopsAIChat (Android)
└─ LoopsAnalyticsDispatcher
├─ LoopsSinkAdapter // always-on → Loops backend (isolated)
└─ customerAdapter // your destination (per-agent config)The Loops sink is always on and isolated — a customer adapter can never
suppress it. You choose the customer destination through LoopsAnalyticsConfig.
Configure destinations
val analytics = LoopsAnalyticsConfig(
loopsSinkEndpoint = "https://<your-ingest-endpoint>",
customerAdapter = HttpWebhookAdapter(endpoint = "https://example.com/collect")
)
val config = LoopsAIChatConfig(agentId = "YOUR_AGENT_ID", analytics = analytics)Built-in adapters
| Adapter | Use |
|---|---|
LoopsSinkAdapter | Always-on delivery to the Loops backend (set via loopsSinkEndpoint). |
HttpWebhookAdapter | Generic POST of the canonical event to your endpoint. |
BlockAnalyticsAdapter | Closure adapter — wire any on-device provider SDK. |
Firebase / Mixpanel / Segment
The core SDK has no third-party analytics dependency. Wire a provider SDK
through BlockAnalyticsAdapter so only apps that need it pull the dependency:
val firebase = BlockAnalyticsAdapter("firebase") { event ->
firebaseAnalytics.logEvent(event.event, event.payload.toBundle())
}
val analytics = LoopsAnalyticsConfig(customerAdapter = firebase)Observe events in your app
Every dispatched event is also surfaced to the host:
override fun onAnalyticsEvent(
fragment: LoopsAIChatFragment,
event: LoopsAnalyticsEvent
) {
// event.event -> canonical name, e.g. "loops_ai_select_item"
// event.channel -> always "android"
// event.payload -> full canonical payload (+ app_version/device/os_version)
}The SDK forces channel: "android" and attaches native context (app_version,
device, os_version) — you don't set these yourself. The provider/destination
is chosen per agent via config, so there's no per-customer native code.
Two event kinds
The callback surfaces two kinds of event, distinguished by payload.kind:
"ecommerce"— the canonical ecommerce events (loops_ai_view_item_list,loops_ai_select_item), which carry anecommerce.itemsarray. See Canonical Events."interaction"— flat customer-interaction events describing the chat and widget funnel. They have noecommerceblock. Previously web-only (via the host dataLayer), they now also cross the native bridge.
| Interaction event | When it fires |
|---|---|
loops_ai_widget_opened | The shopper opens the Loops AI widget. |
loops_ai_chat_started | A conversation begins (first message of a session). |
loops_ai_message_sent | The shopper sends a message. |
loops_ai_product_clicked | The shopper taps a product in the chat. |
loops_ai_product_viewed | A product detail is viewed. |
loops_ai_search_clicked | The shopper taps a search entry point or result. |
Interaction events are opt-in per event on the agent's android channel
analytics config and pass through the same consent gate as ecommerce events. Read
payload.kind in the callback to branch:
override fun onAnalyticsEvent(
fragment: LoopsAIChatFragment,
event: LoopsAnalyticsEvent
) {
when (event.payload.optString("kind")) {
"ecommerce" -> { /* canonical event with ecommerce.items, e.g. "loops_ai_select_item" */ }
"interaction" -> { /* flat event, no ecommerce, e.g. "loops_ai_product_clicked" */ }
else -> { /* missing/unknown kind — ignore or handle defensively */ }
}
}Emit your own native events
The bridge relays the runtime's canonical events automatically. To also push
your app's own events — a commerce funnel (add_to_cart, begin_checkout,
purchase), screen views, etc. — build a dispatcher and emit through the same
pipeline:
val dispatcher = LoopsAnalyticsDispatcher(
sink = LoopsSinkAdapter(endpoint = sinkUrl),
customer = null
)
val event = LoopsAnalyticsEvent.from(
bridgePayload = JSONObject().apply {
put("event", "purchase")
put("currency", "TRY")
put("value", 1299)
},
context = LoopsAnalyticsContext(locale = "en")
)
dispatcher.dispatch(event)LoopsAnalyticsEvent forces channel: "android", stamps schema_version, and
attaches native context — so host-emitted events are indistinguishable from
bridge-relayed ones and merge into the same reports.
Use canonical event names that exist in your analytics schema. Standard GA4
ecommerce names (add_to_cart, purchase) work for commerce funnels; coordinate
any new canonical event with the analytics contract before relying on it in
production. The same analytics taxonomy
applies to web, iOS, and Android.
Next: Architecture.