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