Skip to content
ABTO Guide

Integration guide

Browser JavaScript

An SDK that records Custom Events and LLM traces from the browser.

Client SDKRuns in the browser or a mobile app (Event Key)

The Browser SDK records customer-selected facts the browser can observe directly, such as user actions. It never guesses at models, tokens, or cost. The gateway records those.

Initialize once at the root of your app.

import { defineEvents, initAbto } from '@abto-app/event';
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 },
},
},
});
export const abto = initAbto({
projectKey: 'ek-abto-...',
apiHost: 'https://api.abto.app',
environment: 'production',
events,
});
SettingDefaultPurpose
projectKeyRequiredPublic Event Key that is safe to place in the browser
apiHosthttps://api.abto.appEvent API host; the SDK appends /v1/collect/events
environmentproductiondevelopment warns and sends unregistered events and schema drift
appVersionUnsetAdds $app_version to event context when provided
events{}Custom Event registry created with defineEvents()
capture.promptmetadata_onlyPrompt policy: off, hash, metadata_only, or full
capture.responsemetadata_onlyResponse policy: off, metadata_only, or full

Initialization alone emits no events. It prepares device identity and trace context; call only the Custom Events and LlmTrace events you need at their exact product triggers.

abto.events.ts is the source of truth for your Custom Events. Keeping it in code rather than a dashboard form means changes to the event contract travel through code review and deployment together. Development sends unregistered events and schema drift with a warning. Production drops unregistered events and required/type/enum violations. Ordinary properties not declared in the schema are sent in both environments.

On install, the Browser SDK creates an anonymous device_id and keeps it in the browser. This device_id is the primary axis joining product behavior to AI usage, so the flow connects even without a login.

To also attach a logged-in user, call identify right after login.

abto.identify('user-123', 'tenant-123');
const { deviceId } = abto.getIdentity();

The second tenantId is optional. Later events carry $user_id and, when supplied, $tenant_id; the $device_id value itself is unchanged. Pass getIdentity().deviceId to your backend so the Server SDK can call the Gateway on the same device axis. On logout, call abto.reset() to clear the user context and start a new session. The device stays the same.

abto.forgetDevice() mints a new device_id, but it discards events not yet delivered. Flush first if you need them.

abto.capture('checkout_completed', {
order_id: 'order-123',
value: 49_000,
scale: 'KRW',
});

order_id is not an ABTO field; it is your property describing the order domain. It is required in the declaration above, but you can make it optional or use different properties. Event identifiers are handled by the SDK automatically.

value and its unit label scale are the exception: they are reserved names a Success Metric aggregates. Amounts and counts you want summed or averaged must travel under those two names; sent under any other name the number still reaches the event but is never aggregated.

Joining an AI request to response behavior

Section titled “Joining an AI request to response behavior”

LlmTrace joins prompt submission, response rendering, and later interaction into one flow. The Browser SDK never calls a model directly. Keep the Calling Key and provider keys on your backend, and send only the browser-minted request context to that backend.

const trace = abto.startLlmTrace();
await trace.submitPrompt({
prompt: promptText,
language: 'en',
});
const backendResponse = await fetch('/api/generate', {
method: 'POST',
headers: {
'content-type': 'application/json',
...trace.getHeaders(),
},
body: JSON.stringify({ prompt: promptText }),
});
trace.attachRequestId(backendResponse);
const result = await backendResponse.json();
await trace.markResponseRendered({
responseId: result.responseId,
timeToRenderMs: 1_380,
});
await trace.captureResponseInteraction('copied', {
responseId: result.responseId,
source: 'copy_button',
});

Response interactions accept only copied, inserted, accepted, rejected, shared, downloaded, expanded, collapsed, rated_positive, rated_negative, regenerated, and aborted. The TypeScript literal union and the JavaScript runtime enforce the same list. An unsupported value is warned and dropped before enqueueing; use a Custom Event for product-specific actions.

trace.getHeaders() includes only the browser-owned x-abto-device-id. Your backend validates it, combines it with the actual model call’s featureId in the Server SDK context, and includes the Gateway response’s x-abto-request-id in its response to the browser. The Browser SDK does not mint the server-owned feature ID.

If the backend is on another origin, allow content-type and x-abto-device-id in the preflight response’s Access-Control-Allow-Headers. Also expose x-abto-request-id on the actual response through Access-Control-Expose-Headers so the browser can read it. After attachRequestId(), render and interaction events carry the same $request_id.

  • When localStorage is available, events enter a durable outbox first and are removed only after the server acknowledges them.
  • If the browser blocks localStorage, the SDK uses a memory-only queue that cannot recover after the page closes.
  • The default batch is 20 events, and one collector request carries at most 100.
  • On page exit, only payloads of about 60 KiB or less use fetch(..., { keepalive: true }).
  • Only 408, 429, 5xx, and per-event retry results are retried, with jittered exponential backoff capped at 2 minutes so tabs that fail together do not retry in lockstep.
  • The queue holds at most 1,000 events and drops the oldest on overflow. Backed by the durable outbox, events are never discarded for exceeding an attempt count.
  • Permanent 4xx and per-event drop results are removed from the outbox.
  • The next SDK instance retries events remaining in the localStorage outbox.

The SDK counts send_failed, outbox_write_failed, identity_persist_failed, and storage_unavailable using a fixed vocabulary. These counters ride only in the optional diagnostics field of the next event batch, so they do not create a separate request or a product event. Counters clear only after a successful response and remain for a later batch when delivery fails.

Diagnostics contain only the fixed SDK name and counts by failure kind. They do not copy user IDs, event properties, URLs, or raw error messages. The four-counter schema is bounded without a separate size or recovery layer. With no event batch, the SDK never sends diagnostics alone.

How to name events and design their schemas continues in Event design.