Loops AI Docs

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

AdapterUse
LoopsSinkAdapterAlways-on delivery to the Loops backend (set via loopsSinkEndpoint).
HttpWebhookAdapterGeneric POST of the canonical event to your endpoint.
BlockAnalyticsAdapterClosure 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 an ecommerce.items array. See Canonical Events.
  • "interaction" — flat customer-interaction events describing the chat and widget funnel. They have no ecommerce block. Previously web-only (via the host dataLayer), they now also cross the native bridge.
Interaction eventWhen it fires
loops_ai_widget_openedThe shopper opens the Loops AI widget.
loops_ai_chat_startedA conversation begins (first message of a session).
loops_ai_message_sentThe shopper sends a message.
loops_ai_product_clickedThe shopper taps a product in the chat.
loops_ai_product_viewedA product detail is viewed.
loops_ai_search_clickedThe 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.

On this page