Loops AI Docs

Analytics

Native analytics on the ios 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: "ios", so web and app reports share one taxonomy.

Where events go

LoopsAIChat (iOS)
  └─ 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

let analytics = LoopsAnalyticsConfig(
  loopsSinkEndpoint: URL(string: "https://<your-ingest-endpoint>"),
  customerAdapter: HttpWebhookAdapter(
    endpoint: URL(string: "https://example.com/collect")!
  )
)

let 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:

let firebase = BlockAnalyticsAdapter(id: "firebase") { event in
  Analytics.logEvent(event.event, parameters: event.payload as? [String: Any])
}
let analytics = LoopsAnalyticsConfig(customerAdapter: firebase)

Observe events in your app

Every dispatched event is also surfaced to the host:

func loopsAIChat(_ vc: LoopsAIChatViewController,
                 didEmitAnalyticsEvent event: LoopsAnalyticsEvent) {
  // event.event   -> canonical name, e.g. "loops_ai_select_item"
  // event.channel -> always "ios"
  // event.payload -> full canonical payload (+ app_version/device/os_version)
}

The SDK forces channel: "ios" 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 ios channel analytics config and pass through the same consent gate as ecommerce events. Read payload.kind in the callback to branch:

func loopsAIChat(_ vc: LoopsAIChatViewController,
                 didEmitAnalyticsEvent event: LoopsAnalyticsEvent) {
  switch event.payload["kind"] as? String {
  case "ecommerce":
    // canonical event with ecommerce.items, e.g. "loops_ai_select_item"
    break
  case "interaction":
    // flat event, no ecommerce, e.g. "loops_ai_product_clicked"
    break
  default:
    break
  }
}

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. This is the native equivalent of the web trackCanonicalEvent:

let dispatcher = LoopsAnalyticsDispatcher(
  sink: LoopsSinkAdapter(endpoint: sinkURL),
  customer: nil
)

let event = LoopsAnalyticsEvent(
  event: "purchase",
  params: [
    "currency": "TRY",
    "value": 1299,
    "ecommerce": ["items": [["item_id": "SKU-123", "quantity": 1]]],
  ],
  context: LoopsAnalyticsContext(locale: "en")
)

dispatcher.dispatch(event)

LoopsAnalyticsEvent(event:params:context:) forces channel: "ios", 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.

Next: Architecture.

On this page