SDK API reference
The package exports exactly four functions. It has zero runtime dependencies, ships its own types, and is designed never to throw and never to break the host page.
import { init, ready, getVariant, track } from '@absolutely-butter/sdk'init(config)
init(config: {
apiKey: string
experimentId: string
baseUrl: string
timeout?: number // default: 2000
}): Promise<void>Call once, on the client, as early as possible. Returns a Promise<void> that always resolves — on success, on network failure, on timeout, on a malformed response, or when the experiment isn't live. It never rejects.
| Field | Type | Notes |
|---|---|---|
apiKey | string | Your publishable pk_live_… key. Sent as Authorization: Bearer on every request. Safe to ship in client code. |
experimentId | string | The exp_… identifier from the experiment's detail page. |
baseUrl | string | Origin of your Absolutely Butter API, e.g. https://abserver-production.up.railway.app. |
timeout | number? | Milliseconds before the config request is aborted and control is served. Default 2000. |
What init() does, in order:
- Reads the
__ab_<experimentId>cookie. If it holdscontrolorvariant, that value is used, the session id is restored from__ab_<experimentId>_sid, andinit()resolves immediately — no network request. - Otherwise it fetches
GET {baseUrl}/v1/experiments/<id>/config. - If the response status isn't
"live", it resolves and leaves the variant ascontrol. No cookie is set, no impression is sent. - If it is live, the SDK draws one sample from each arm's Beta posterior (Thompson sampling), picks the higher draw, writes both cookies (30-day expiry), and sends one
impressionevent.
// GET {baseUrl}/v1/experiments/:id/config
// Authorization: Bearer <apiKey>
{
"status": "live",
"control": { "alpha": 15, "beta": 90 },
"variant": { "alpha": 24, "beta": 72 }
}
// For any non-live experiment the body is just:
{ "status": "draft" }ready()
ready(): Promise<void>Resolves once init() has settled — whether it succeeded or failed silently. Use it to defer rendering until the variant is known. It never rejects. Before init() has been called, the promise is simply still pending.
getVariant()
getVariant(): 'control' | 'variant'Synchronous. Returns the assigned arm. This is the control guarantee:
| State | getVariant() |
|---|---|
Before init() resolves | 'control' |
| After a successful assignment | 'control' or 'variant' |
| Network failure / timeout | 'control' |
| Invalid API key or experiment id | 'control' |
| Experiment not live | 'control' |
It never returns null, never returns any other string, and never throws.
track(event)
track(event: 'conversion'): voidRecords a conversion for the current session. Fire-and-forget: it returns void, sends the request without awaiting it, and swallows every error. 'conversion' is the only accepted argument.
It does nothing if there is no session id — which happens when init() never established a live session, or when a returning visitor's _sid cookie was cleared. The server also ignores a conversion with no matching impression, and ignores repeat conversions from a session that already converted.
Cookies
The SDK sets two cookies per experiment, both scoped to path=/:
| Name | Value | Purpose |
|---|---|---|
__ab_<experimentId> | control or variant | Keeps assignment stable across page loads. |
__ab_<experimentId>_sid | a random UUID | Server-side deduplication of impressions. Not a user id — it cannot be traced to a person. |
- Expiry: 30 days (
max-age), refreshed only when a new assignment is made. - Flags:
SameSite=Lax. NotHttpOnly(the SDK must read them in JS) and notSecure-only (so they work onlocalhost). - If cleared mid-experiment: the next
init()sees no cookie and re-runs assignment. The visitor may land on the other variant and receives a new session id. This is an accepted trade-off of client-side assignment.
How assignment works
The /config response carries each arm's Beta posterior parameters — alpha = 1 + conversions and beta = 1 + (impressions − conversions). The SDK draws one sample from each arm and assigns whichever is higher. Early on, when the posteriors are wide, this is close to a coin flip; as data accumulates it shifts traffic toward the better-performing arm. There is no epsilon parameter and no fixed split.
Flicker
Assignment finishes after the page has already rendered, so a visitor who ends up on variant briefly sees controlfirst, then a re-render. It is a control → variant flash, never a blank one. Two ways to handle it:
- Default to control (recommended).
getVariant()already returnscontrolbefore assignment, so the original renders instantly and swaps in the variant wheninit()resolves. On fast connections this is imperceptible, and it degrades gracefully on slow ones. - Hide and reveal. Keep the experimental region hidden,
await ready(), then reveal it. No flash, at the cost of a blank region while the config request is in flight.
import { init, ready } from '@absolutely-butter/sdk'
const box = document.querySelector('#hero')
box.style.visibility = 'hidden'
init({ apiKey: '…', experimentId: '…', baseUrl: 'https://abserver-production.up.railway.app' })
await ready()
box.style.visibility = 'visible' // reveal once the variant is knownZero-flicker assignment at the edge (before the page renders) is on the v2 roadmap. v1 is client-side only.
Endpoints the SDK calls
GET {baseUrl}/v1/experiments/<id>/config— oninit()when there is no cookie.Authorization: Bearer <apiKey>.POST {baseUrl}/v1/events— oneimpressionon assignment, oneconversionpertrack()call.Authorization: Bearer <apiKey>. Always answered202; the SDK does not wait on it.
Full request and response shapes are in the statistics reference and the Server & API specification.