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.

ts
import { init, ready, getVariant, track } from '@absolutely-butter/sdk'

init(config)

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

FieldTypeNotes
apiKeystringYour publishable pk_live_… key. Sent as Authorization: Bearer on every request. Safe to ship in client code.
experimentIdstringThe exp_… identifier from the experiment's detail page.
baseUrlstringOrigin of your Absolutely Butter API, e.g. https://abserver-production.up.railway.app.
timeoutnumber?Milliseconds before the config request is aborted and control is served. Default 2000.

What init() does, in order:

  1. Reads the __ab_<experimentId> cookie. If it holds control or variant, that value is used, the session id is restored from __ab_<experimentId>_sid, and init() resolves immediately — no network request.
  2. Otherwise it fetches GET {baseUrl}/v1/experiments/<id>/config.
  3. If the response status isn't "live", it resolves and leaves the variant as control. No cookie is set, no impression is sent.
  4. 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 impression event.
json
// 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()

ts
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()

ts
getVariant(): 'control' | 'variant'

Synchronous. Returns the assigned arm. This is the control guarantee:

StategetVariant()
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)

ts
track(event: 'conversion'): void

Records 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=/:

NameValuePurpose
__ab_<experimentId>control or variantKeeps assignment stable across page loads.
__ab_<experimentId>_sida random UUIDServer-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. Not HttpOnly (the SDK must read them in JS) and not Secure-only (so they work on localhost).
  • 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:

  1. Default to control (recommended). getVariant() already returns control before assignment, so the original renders instantly and swaps in the variant when init() resolves. On fast connections this is imperceptible, and it degrades gracefully on slow ones.
  2. 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.
ts
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 known

Zero-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 — on init() when there is no cookie. Authorization: Bearer <apiKey>.
  • POST {baseUrl}/v1/events — one impression on assignment, one conversion per track() call. Authorization: Bearer <apiKey>. Always answered 202; the SDK does not wait on it.

Full request and response shapes are in the statistics reference and the Server & API specification.