Estimated time: under 15 minutes.
Quick start
This walks through a complete integration in four steps — install, init, getVariant, track — using Next.js App Router. The SDK is framework-agnostic; any browser environment works the same way.
Before you start
You need a live experiment. In the dashboard:
- Create an experiment (Experiments → New) — name it, describe your control and variant, and define the conversion goal. It starts in
draft. - Open the experiment and Launch it. It must be
livebefore the SDK will assign variants or record events. - Copy its ID (the
exp_…value in the URL and on the detail page). - Copy your API key from Settings → API key. It looks like
pk_live_…and is safe to expose in client code.
If the experiment isn't live
The config endpoint returns only a status for non-live experiments. The SDK then serves control to everyone and sends no impressions — so your dashboard counts stay at zero.
1. Install
npm install @absolutely-butter/sdkThe package has zero runtime dependencies and ships its own TypeScript types. pnpm add and yarn add work too.
2. Initialize
Call init() once, as early as possible on the client. It reads a cookie, and on a cache miss fetches the experiment config and assigns a variant. It returns a Promise<void> that resolves whether assignment succeeds or fails — it never rejects.
import { init } from '@absolutely-butter/sdk'
init({
apiKey: process.env.NEXT_PUBLIC_AB_API_KEY!, // your publishable pk_live_… key
experimentId: 'exp_xxxxxxxxxxxx', // from the experiment's detail page
baseUrl: 'https://abserver-production.up.railway.app',
})baseUrl is the origin of your Absolutely Butter API. timeout is optional and defaults to 2000 ms; if the config request is slower than that, the SDK gives up and serves control.
3. Render the variant
getVariant() is synchronous and returns 'control' or 'variant'. Before init() resolves — and on any failure — it returns 'control', so the original experience always renders by default.
import { getVariant } from '@absolutely-butter/sdk'
const variant = getVariant() // 'control' | 'variant' — never throws, never null
if (variant === 'variant') {
// render the change you're testing
} else {
// render the original ('control' is also the value before init resolves)
}4. Track the conversion
Call track('conversion') when the visitor completes your goal. It is fire-and-forget — it returns void, swallows all errors, and does nothing if the visitor has no assigned session (for example, a returning visitor whose session cookie was cleared).
import { track } from '@absolutely-butter/sdk'
<button onClick={() => track('conversion')}>Sign up</button>Complete example
One client component that initializes the SDK, waits for ready(), renders the assigned variant, and tracks a conversion on click:
'use client'
import { useEffect, useState } from 'react'
import { init, ready, getVariant, track } from '@absolutely-butter/sdk'
const EXPERIMENT_ID = 'exp_xxxxxxxxxxxx'
export default function SignupCta() {
const [variant, setVariant] = useState<'control' | 'variant'>('control')
useEffect(() => {
init({
apiKey: process.env.NEXT_PUBLIC_AB_API_KEY!,
experimentId: EXPERIMENT_ID,
baseUrl: 'https://abserver-production.up.railway.app',
})
// ready() always resolves — on success or on silent failure
ready().then(() => setVariant(getVariant()))
}, [])
return (
<button
className={variant === 'variant' ? 'btn-green' : 'btn-blue'}
onClick={() => track('conversion')}
>
{variant === 'variant' ? 'Start your free trial' : 'Sign up'}
</button>
)
}Verify it worked
Load the page in a browser, then open the experiment's detail screen in the dashboard:
- Impressions increment on first assignment. Refreshing the same browser does not add more — impressions are deduplicated per session.
- Conversions increment the first time that session fires
track('conversion'). Repeat conversions from the same session are ignored.
Your integration is fully confirmed once the experiment is live and has recorded at least one impression and at least one conversion.
One thing to expect
Because assignment happens after the page loads, a visitor who ends up on variant sees control for a moment first, then a re-render. This is a control → variant flash, not a blank flash. The SDK reference covers two ways to handle it.