Integration guide
Event design
How to name and declare your events, and how they are kept apart from ABTO system events.
Events are the raw material of success metrics. How you name them and declare their properties decides how easy it will be to build metrics and trust the numbers later.
Two kinds of events
Section titled “Two kinds of events”- Name starts with
$:$pageview,$ai_prompt_submitted - Defined by ABTO, emitted only by an explicit SDK helper or SDK helper
- Named in your product's language without a
$:checkout_completed,summary_copied - Defined by you, sent with
capture
Following one event end to end
Section titled “Following one event end to end”Before digging into design, it helps to trace the road one event travels
from a user’s screen to a number on the dashboard.
There are four stretches — name it, place it in the client, join it to AI calls by device_id, and meet it on the dashboard.
1. Declare the name
Section titled “1. Declare the name”abto.events.ts in your repository is the starting point.
A name that is missing here is dropped in production, so this stretch cannot be skipped.
export const events = defineEvents({ summary_copied: { properties: { document_id: { type: 'string', required: true }, }, },});2. Place it in the client
Section titled “2. Place it in the client”Keep only the Event Key (ek-abto-…) in the client.
A Calling Key placed here rides the bundle onto your users’ devices.
In the browser, pass the declared events to initialization and call it where the behavior happens.
export const abto = initAbto({ projectKey: 'ek-abto-...', apiHost: 'https://api.abto.app', environment: 'production', events,});
abto.capture('summary_copied', { document_id: 'doc-123' });Mobile follows the same grain. On Android:
abto.capture("summary_copied", mapOf("document_id" to "doc-123"))Either way the SDK buffers the event on the device before sending, so the call returns immediately instead of waiting on the network.
3. Join it to AI calls by device_id
Section titled “3. Join it to AI calls by device_id”This is the seam of the whole flow.
A client SDK mints an anonymous device_id the moment it is installed and keeps it on the device,
and that value is the only axis joining product behavior to AI calls.
So your server must carry the same value when it calls the gateway.
Read it with abto.getIdentity().deviceId in the browser or abto.deviceId on mobile,
pass it to your backend, and put it in the Server SDK’s context.
await abto.withContext( { deviceId, featureId: 'review.summary' }, async () => { /* the model call */ },);That deviceId goes out as the x-abto-device-id header.
Put an arbitrary value such as a login id there and it will differ from the device_id the client sent,
so behavior and calls never meet.
To narrow down to reactions to a single response, send the x-abto-request-id the gateway returns
back down to the browser. Renders, copies, and feedback for that response bind by the same request_id.
4. Meet it on the dashboard
Section titled “4. Meet it on the dashboard”The two records arrive by different roads.
The gateway records a call’s cost, latency, and tokens as it forwards the call,
while the client SDK uploads the event you just sent.
The server meshes the two by device_id and stands them on one screen.
Confirm arrival in the event table at the bottom of Success metrics. Events sent by a client appear there automatically, so a name in the table means it was received. If it is missing, the event has either never fired or was dropped for disagreeing with the declaration; the FAQ walks through the causes.
Once the name is in the table it becomes material for a metric. Ratio metrics count people rather than calls, so “what percentage of users who got this option copied the summary” splits cleanly per device.
Schemas live in your code
Section titled “Schemas live in your code”Custom Event names and properties are declared in abto.events.ts in your repository.
export const events = defineEvents({ checkout_completed: { properties: { order_id: { type: 'string', required: true }, value: { type: 'number', required: true }, scale: { type: 'string', enum: ['KRW', 'USD'], required: true }, }, },});The reason declarations live in code rather than in a dashboard form is that changes to the event contract then travel through code review and deployment together.
When you need a product-domain ID such as an order, document, or user,
declare it as a property like order_id above and send it yourself.
Your declaration also decides whether it is required.
Amounts and counts that a Success Metric will sum or average
must travel under the reserved names value (number) and scale (unit label, string).
Other names such as amount or currency still reach the event,
but they are not aggregated and the event counts only as a conversion.
Development is lenient, production is strict
Section titled “Development is lenient, production is strict”| Situation | Development | Production |
|---|---|---|
| Unregistered event | Sent, with a discovery warning | Dropped |
| required/type/enum violation | Sent, with a drift warning | Dropped |
| Ordinary property not in the schema | Sent | Sent |
Custom Property with a $ name | Not sent | Not sent |
During development, unregistered events are still sent with a warning so nothing blocks your work. In production, events that disagree with the declaration are dropped so the data stays clean.
Joining AI responses to user behavior
Section titled “Joining AI responses to user behavior”Ordinary behavior such as a purchase or a copy joins AI usage through device_id, so there is
nothing extra to do. To narrow it to “what the person who saw this response did next”,
take the x-abto-request-id from the gateway response on your server and pass it down to the browser.
Render, copy, and feedback interactions on that response then share the same request_id.
The SDK guards against loss and duplication
Section titled “The SDK guards against loss and duplication”The SDK keeps events on the device until delivery and removes them only after the server confirms receipt. Transient network errors are retried automatically and the server filters out duplicates, so even on an unstable connection events are neither lost nor counted twice.
Sessions
Section titled “Sessions”Sessions need no instrumentation of their own.
Session boundaries are derived automatically from each event’s $session_id and timestamp.